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.
471 lines
15 KiB
Markdown
471 lines
15 KiB
Markdown
# 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**
|
|
- ✅ `ServerInfoDto` with nested `VersionInfo` struct (canonical, release, build)
|
|
- ✅ `ServerInfo` model with `features: [String]?` and `supportsOAuth` computed property
|
|
- ✅ `GetServerInfoUseCase` with optional endpoint parameter
|
|
- ✅ `InfoApiClient` with endpoint parameter logic (custom endpoint vs TokenProvider)
|
|
- ✅ Unit tests for ServerInfo and feature detection
|
|
- ✅ Backward compatibility with old servers
|
|
|
|
**Phase 2: OAuth Core**
|
|
- ✅ `PKCEGenerator` with verifier/challenge generation
|
|
- ✅ OAuth DTOs: `OAuthClientCreateDto`, `OAuthClientResponseDto`, `OAuthTokenRequestDto`, `OAuthTokenResponseDto`
|
|
- ✅ Domain models: `OAuthClient`, `OAuthToken`, `AuthenticationMethod`
|
|
- ✅ `API.swift` extended with OAuth methods (`registerOAuthClient`, `exchangeOAuthToken`)
|
|
- ✅ `OAuthRepository` + `POAuthRepository` protocol
|
|
- ✅ `OAuthManager` orchestrates OAuth flow
|
|
- ✅ Unit tests for PKCEGenerator
|
|
|
|
**Phase 3: Browser Integration**
|
|
- ✅ `OAuthSession` wraps `ASWebAuthenticationSession`
|
|
- ✅ `OAuthFlowCoordinator` manages 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**
|
|
- ✅ `AuthenticationMethod` enum (apiToken, oauth)
|
|
- ✅ `TokenProvider` extended with OAuth methods (`getOAuthToken`, `setOAuthToken`, `getAuthMethod`, `setAuthMethod`)
|
|
- ✅ `setEndpoint(_ endpoint: String)` added to TokenProvider
|
|
- ✅ `KeychainHelper` extended with OAuth token storage
|
|
- ✅ `KeychainTokenProvider` handles both token types
|
|
- ✅ `AuthRepository` extended with `loginWithOAuth`, `getAuthenticationMethod`, `switchToClassicAuth`
|
|
- ✅ Endpoint now saved in TokenProvider (was missing before)
|
|
|
|
**Phase 5: UI & UX (Partially Complete)**
|
|
- ✅ `OnboardingServerView` refactored 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
|
|
- ✅ `SettingsServerViewModel` extended with `checkServerOAuthSupport()` and `loginWithOAuth()`
|
|
- ✅ `LoginWithOAuthUseCase` created
|
|
- ✅ Factories updated (DefaultUseCaseFactory, MockUseCaseFactory)
|
|
- ❌ LoginMethodSelectionView removed (no longer needed - OAuth is automatic)
|
|
|
|
**Removed/Changed:**
|
|
- ❌ All `#if os(iOS) && !APP_EXTENSION` checks removed
|
|
- ❌ `LoginMethodSelectionView` removed - OAuth is now attempted automatically
|
|
- ❌ `showLoginMethodSelection` flag removed
|
|
- ✅ Direct OAuth attempt with fallback to classic login on error
|
|
|
|
---
|
|
|
|
## 🎯 Updated Goals
|
|
|
|
1. ~~**User Choice**~~ **Automatic OAuth**: OAuth is attempted automatically if server supports it
|
|
2. **Auto-Detection**: ✅ Automatically detect if server supports OAuth via `/info` endpoint
|
|
3. **Security**: ✅ OAuth 2.0 Authorization Code Flow with PKCE (S256)
|
|
4. **UX**: ✅ Seamless browser-based authentication using `ASWebAuthenticationSession`
|
|
5. **Fallback**: ✅ Graceful fallback to username/password if OAuth fails or is not supported
|
|
6. **Migration Path**: ❌ Not yet implemented for existing users
|
|
|
|
---
|
|
|
|
## 🔍 API Analysis
|
|
|
|
### Server Info Response (Actual Format)
|
|
|
|
```json
|
|
{
|
|
"version": {
|
|
"canonical": "0.21.4",
|
|
"release": "0.21.4",
|
|
"build": ""
|
|
},
|
|
"features": ["oauth"]
|
|
}
|
|
```
|
|
|
|
**Changes from Original Plan:**
|
|
- `version` is now an object, not a string
|
|
- `buildDate` and `userAgent` removed (not in actual response)
|
|
- `features` is optional (only in newer servers)
|
|
|
|
### OAuth Endpoints
|
|
|
|
1. **Feature Detection**: `GET /api/info` (no auth)
|
|
2. **Client Registration**: `POST /api/oauth/client` (no auth)
|
|
3. **Authorization**: `GET /authorize` (browser, web page)
|
|
4. **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)
|
|
- `AuthenticationMethod` enum (apiToken, oauth)
|
|
|
|
**Data Layer:**
|
|
- `InfoApiClient` - handles `/api/info` with optional endpoint parameter
|
|
- `API` - extended with OAuth client registration and token exchange
|
|
- `OAuthRepository` - orchestrates OAuth API calls
|
|
- `OAuthManager` - business logic for OAuth flow
|
|
- `OAuthSession` - wraps ASWebAuthenticationSession
|
|
- `OAuthFlowCoordinator` - coordinates complete 5-phase flow
|
|
|
|
**Use Cases:**
|
|
- `GetServerInfoUseCase(endpoint: String?)` - get server info with optional custom endpoint
|
|
- `LoginWithOAuthUseCase` - execute OAuth login flow
|
|
- `CheckServerReachabilityUseCase` - 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)
|
|
- ✅ `ServerInfoDto` with `VersionInfo` nested struct
|
|
- ✅ `ServerInfo.features` optional array
|
|
- ✅ `supportsOAuth` computed property
|
|
- ✅ `GetServerInfoUseCase` with 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
|
|
|
|
1. **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
|
|
|
|
2. **Endpoint Parameter Flow Needs Verification**
|
|
- `GetServerInfoUseCase.execute(endpoint:)` receives endpoint parameter
|
|
- `ServerInfoRepository.getServerInfo(endpoint:)` passes it through
|
|
- `InfoApiClient.getServerInfo(endpoint:)` uses it OR falls back to TokenProvider
|
|
- Need to verify this chain works correctly
|
|
|
|
### Minor Issues
|
|
|
|
3. **No Migration UI for Existing Users**
|
|
- Users with API tokens can't migrate to OAuth yet
|
|
- Settings screen needs "Switch to OAuth" button
|
|
|
|
4. **No OAuth Token Display**
|
|
- Can't see which auth method is active
|
|
- Can't see token expiry or scopes
|
|
|
|
5. **All `#if` Checks Removed**
|
|
- Removed all `#if os(iOS) && !APP_EXTENSION` conditionals
|
|
- May cause issues with URLShare extension (needs verification)
|
|
|
|
---
|
|
|
|
## ✅ Updated TODO List
|
|
|
|
### 🔴 IMMEDIATE (Critical for functionality)
|
|
|
|
- [ ] **Verify API Integration with OAuth Token**
|
|
- [ ] Check that `API.swift` uses `await 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
|
|
|
|
- [ ] **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
|
|
|
|
1. **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
|
|
|
|
2. **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
|
|
|
|
3. **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
|
|
|
|
4. **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:**
|
|
```swift
|
|
// 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:**
|
|
```swift
|
|
// KeychainTokenProvider
|
|
func getToken() async -> String? {
|
|
if let method = await getAuthMethod(), method == .oauth {
|
|
return await getOAuthToken()?.accessToken
|
|
}
|
|
return keychainHelper.loadToken() // API token
|
|
}
|
|
```
|
|
|
|
**OAuth Flow:**
|
|
```swift
|
|
// 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
|
|
|
|
1. **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
|
|
|
|
2. **Manual Testing**
|
|
- Test OAuth flow end-to-end
|
|
- Test fallback to classic login
|
|
- Test on real device
|
|
|
|
3. **Settings Screen**
|
|
- Show current auth method
|
|
- Add migration button
|
|
|
|
4. **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
|