# Architecture Overview ## Design Philosophy This application follows iOS best practices: - **MVVM Pattern**: Separation of UI and business logic - **SwiftUI**: Declarative UI framework - **Combine**: Reactive state management via `@Published` - **async/await**: Modern concurrency - **No External Dependencies**: Uses only native iOS frameworks ## Component Diagram ``` ┌─────────────────────────────────────────────────────────┐ │ SwiftUI Views │ │ ┌──────────┐ ┌──────────────────┐ ┌──────────────┐ │ │ │ LoginView│ │AuthenticatedView │ │CreatePostView│ │ │ └─────┬────┘ └────────┬─────────┘ └──────┬───────┘ │ └────────┼────────────────┼────────────────────┼─────────┘ │ │ │ └────────────────┼────────────────────┘ │ ┌────────────────▼────────────────┐ │ AuthenticationManager │ │ (Observable ViewModel) │ └────────────────┬────────────────┘ │ ┌────────────────┼────────────────┐ │ │ │ ┌────▼─────┐ ┌────▼────┐ ┌────▼────┐ │OAuthClient│ │XRPCClient│ │Identity │ │ │ │ │ │Resolver │ └─────┬─────┘ └────┬────┘ └────┬────┘ │ │ │ ┌─────┼───────────────┼──────────────┼─────┐ │ │ │ │ │ │ ┌──▼──┐ ┌────▼────┐ ┌─────▼─────┐ │ │ │PKCE │ │ DPoP │ │ Keychain │ │ │ │Gen │ │Generator│ │ Manager │ │ │ └─────┘ └─────────┘ └───────────┘ │ │ │ │ Core Authentication │ └──────────────────────────────────────────┘ │ ┌───────────┼───────────┐ │ │ │ ┌────▼─────┐ ┌──▼───┐ ┌────▼────┐ │ URLSession│ │Security│ │CryptoKit│ └───────────┘ └────────┘ └─────────┘ Native iOS Frameworks ``` ## Layer Breakdown ### 1. Presentation Layer (Views/) **Purpose**: User interface and user interaction **Components**: - `ContentView.swift` - Root view, routing between login/authenticated states - `LoginView.swift` - Handle input and sign-in button - `AuthenticatedView.swift` - Post-login dashboard - `CreatePostView.swift` - Demo XRPC functionality **Responsibilities**: - Render UI based on state - Capture user input - Trigger ViewModel actions - Display loading/error states **Pattern**: Declarative SwiftUI with `@EnvironmentObject` for state ### 2. ViewModel Layer (Authentication/) **Component**: `AuthenticationManager.swift` **Purpose**: Coordinate authentication and manage app state **Responsibilities**: - Orchestrate OAuth flow - Manage authentication state - Provide XRPC client to views - Handle errors and present to UI - Check for existing sessions **State Properties**: ```swift @Published var isAuthenticated: Bool @Published var userDID: String? @Published var userHandle: String? @Published var isLoading: Bool @Published var errorMessage: String? ``` **Key Methods**: - `signIn(handle:)` - Initiate OAuth flow - `signOut()` - Clear session - `getXRPCClient()` - Provide authenticated API client ### 3. Authentication Layer (Authentication/) #### OAuthClient.swift **Purpose**: Implement complete OAuth 2.1 flow **OAuth Steps**: 1. Resolve handle → DID 2. Fetch DID document → PDS URL 3. Discover authorization server 4. Generate PKCE parameters 5. Perform PAR (Pushed Authorization Request) 6. Present ASWebAuthenticationSession 7. Exchange authorization code for tokens 8. Verify identity (DID match) 9. Store tokens securely **Key Security Features**: - PKCE with S256 challenge - State parameter for CSRF protection - DPoP nonce handling - Identity verification #### PKCEGenerator.swift **Purpose**: Generate PKCE parameters **Functions**: - `generateCodeVerifier()` - 32-byte random string - `generateCodeChallenge(from:)` - S256 hash of verifier **Why**: Prevents authorization code interception attacks #### DPoPGenerator.swift **Purpose**: Create DPoP JWT proofs **Functions**: - `generateProof(method:url:nonce:accessToken:)` - Creates ES256 JWT **JWT Structure**: ```json { "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", ... } } { "jti": "unique-id", "htm": "POST", "htu": "https://...", "iat": 1234567890, "nonce": "server-nonce", "ath": "base64url(SHA256(access_token))" } ``` **Why**: Binds tokens to specific keys, prevents replay attacks #### IdentityResolver.swift **Purpose**: Resolve handles and discover services **Functions**: - `resolveHandle(_:)` - Handle → DID via HTTPS well-known - `fetchDIDDocument(_:)` - DID → DID Document - `discoverAuthorizationServer(pdsURL:)` - Find OAuth server - `fetchAuthServerMetadata(authServerURL:)` - Get OAuth endpoints **Discovery Chain**: ``` Handle → DID → DID Doc → PDS URL → Auth Server → Metadata ``` #### KeychainManager.swift **Purpose**: Secure token storage **Functions**: - `save(_:forKey:)` - Store token in Keychain - `retrieve(forKey:)` - Get token from Keychain - `delete(forKey:)` - Remove token - `clearAll()` - Clear all stored tokens **Security**: - Uses `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` - Tokens never leave secure storage - Never logged or exposed ### 4. Networking Layer (Networking/) #### XRPCClient.swift **Purpose**: Make authenticated XRPC API calls **Features**: - Adds DPoP header with access token hash - Adds Authorization header with DPoP token - Handles DPoP nonce updates - Error handling **Example Flow** (Create Post): ``` 1. Get access token from Keychain 2. Get user DID from Keychain 3. Generate DPoP proof with token hash 4. Build XRPC request 5. Add headers: Authorization + DPoP 6. Send request 7. Update DPoP nonce from response 8. Return response ``` ### 5. Data Layer (Models/) #### OAuthModels.swift **Purpose**: OAuth data structures **Key Models**: - `OAuthTokenResponse` - Token endpoint response - `AuthorizationServerMetadata` - OAuth server config - `ResourceServerMetadata` - PDS OAuth config - `PARResponse` - PAR request result #### DIDDocument.swift **Purpose**: DID document structure **Extensions**: - `pdsEndpoint` - Extract PDS URL from services - `handle` - Extract handle from alsoKnownAs #### XRPCModels.swift **Purpose**: XRPC request/response structures **Models**: - `CreateRecordRequest` - Post creation request - `CreateRecordResponse` - Created record info ### 6. Utilities (Utilities/) #### Constants.swift **Purpose**: App-wide configuration **Key Constants**: - URL scheme for OAuth callback - Client ID (dev mode uses localhost) - Keychain service identifier - OAuth parameters ## Data Flow: Sign In ``` ┌────────────────────────────────────────────────────────┐ │ 1. User enters handle in LoginView │ └───────────────────────┬────────────────────────────────┘ │ ┌───────────────────────▼────────────────────────────────┐ │ 2. AuthenticationManager.signIn(handle:) │ └───────────────────────┬────────────────────────────────┘ │ ┌───────────────────────▼────────────────────────────────┐ │ 3. OAuthClient.authenticate(handle:) │ │ ├─ IdentityResolver.resolveHandle() │ │ ├─ IdentityResolver.fetchDIDDocument() │ │ ├─ IdentityResolver.discoverAuthorizationServer() │ │ ├─ PKCEGenerator.generateCodeVerifier() │ │ ├─ PKCEGenerator.generateCodeChallenge() │ │ ├─ performPAR() with DPoP │ │ ├─ presentAuthorizationUI() │ │ ├─ exchangeCodeForTokens() with PKCE │ │ └─ verify DID matches │ └───────────────────────┬────────────────────────────────┘ │ ┌───────────────────────▼────────────────────────────────┐ │ 4. KeychainManager.save() tokens │ └───────────────────────┬────────────────────────────────┘ │ ┌───────────────────────▼────────────────────────────────┐ │ 5. AuthenticationManager updates @Published state │ └───────────────────────┬────────────────────────────────┘ │ ┌───────────────────────▼────────────────────────────────┐ │ 6. SwiftUI re-renders → AuthenticatedView │ └────────────────────────────────────────────────────────┘ ``` ## Data Flow: Create Post ``` ┌────────────────────────────────────────────────────────┐ │ 1. User enters text in CreatePostView │ └───────────────────────┬────────────────────────────────┘ │ ┌───────────────────────▼────────────────────────────────┐ │ 2. CreatePostView calls createPost() │ │ ├─ Get DID from AuthenticationManager │ │ ├─ Resolve DID to DID Document │ │ └─ Extract PDS URL │ └───────────────────────┬────────────────────────────────┘ │ ┌───────────────────────▼────────────────────────────────┐ │ 3. Get XRPCClient from AuthenticationManager │ └───────────────────────┬────────────────────────────────┘ │ ┌───────────────────────▼────────────────────────────────┐ │ 4. XRPCClient.createPost(text:pdsURL:) │ │ ├─ Get access token from Keychain │ │ ├─ Generate DPoP proof with token hash │ │ ├─ Build CreateRecordRequest │ │ ├─ Add Authorization: DPoP │ │ ├─ Add DPoP: │ │ └─ POST to /xrpc/com.atproto.repo.createRecord │ └───────────────────────┬────────────────────────────────┘ │ ┌───────────────────────▼────────────────────────────────┐ │ 5. PDS processes request, returns CreateRecordResponse │ └───────────────────────┬────────────────────────────────┘ │ ┌───────────────────────▼────────────────────────────────┐ │ 6. Update DPoP nonce, show success to user │ └────────────────────────────────────────────────────────┘ ``` ## Security Architecture ### Token Security - **Storage**: iOS Keychain with `WhenUnlockedThisDeviceOnly` - **Transport**: HTTPS only - **Binding**: DPoP binds tokens to ephemeral key pairs - **Lifetime**: Access tokens expire, refresh token rotates ### OAuth Security - **PKCE**: Prevents code interception (mandatory) - **State**: Prevents CSRF attacks - **PAR**: Prevents parameter tampering - **DPoP**: Prevents token replay and theft ### Network Security - **HTTPS**: All network requests - **Certificate Pinning**: Not implemented (add for production) - **No Logs**: Tokens never logged ## State Management Uses Combine framework through SwiftUI: - `@Published` properties in ViewModel - `@EnvironmentObject` for dependency injection - `@StateObject` for ViewModel ownership - `@State` for local view state ## Concurrency Model Uses Swift concurrency (async/await): - All network calls are `async` - UI updates via `@MainActor` - No explicit threading needed - Structured concurrency with Tasks ## Error Handling **Strategy**: Typed errors propagate up, converted to user-friendly messages **Error Types**: - `OAuthError` - OAuth flow errors - `IdentityError` - Resolution failures - `KeychainError` - Storage errors - `XRPCError` - API errors **User Experience**: - All errors show friendly messages - Loading states during operations - Cancel handling for OAuth flow ## Testing Considerations **Unit Testable Components**: - `PKCEGenerator` - Pure functions - `DPoPGenerator` - JWT generation - `IdentityResolver` - With mock URLSession - `KeychainManager` - With test keychain **Integration Testing**: - OAuth flow with test account - XRPC calls to PDS - End-to-end authentication ## Performance Considerations **Optimizations**: - DPoP key pair created once per session - Tokens cached in memory (ViewModel) - Minimal network requests **Areas for Improvement**: - Token refresh before expiry - Cache DID documents - Parallel requests where possible ## Extensibility **Easy to Add**: - New XRPC methods (add to XRPCClient) - New OAuth scopes (update Constants) - Additional views (follow existing pattern) - Token refresh (add to OAuthClient) **Architecture Supports**: - Multiple accounts (key by DID) - Background refresh - Deep linking - Share extensions ## Production Readiness **Current State**: Development demo **For Production, Add**: - Token refresh implementation - Certificate pinning - Error reporting (e.g., Sentry) - Analytics - Rate limiting - Proper client metadata hosting - Biometric authentication - Background token refresh - Unit and integration tests ## Key Design Decisions 1. **Native Only**: No external dependencies for maintainability 2. **MVVM**: Clear separation of concerns 3. **Keychain**: Secure by default 4. **ASWebAuthenticationSession**: Standard, secure, maintains cookies 5. **SwiftUI**: Modern, declarative, type-safe 6. **async/await**: Modern concurrency, easier to read 7. **Development Mode**: Easy testing without hosting infrastructure ## Conclusion This architecture provides a solid foundation for ATProtocol applications on iOS, demonstrating best practices for OAuth 2.1, security, and modern iOS development patterns.