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 statesLoginView.swift- Handle input and sign-in buttonAuthenticatedView.swift- Post-login dashboardCreatePostView.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:
@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 flowsignOut()- Clear sessiongetXRPCClient()- Provide authenticated API client
3. Authentication Layer (Authentication/)#
OAuthClient.swift#
Purpose: Implement complete OAuth 2.1 flow
OAuth Steps:
- Resolve handle → DID
- Fetch DID document → PDS URL
- Discover authorization server
- Generate PKCE parameters
- Perform PAR (Pushed Authorization Request)
- Present ASWebAuthenticationSession
- Exchange authorization code for tokens
- Verify identity (DID match)
- 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 stringgenerateCodeChallenge(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:
{
"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-knownfetchDIDDocument(_:)- DID → DID DocumentdiscoverAuthorizationServer(pdsURL:)- Find OAuth serverfetchAuthServerMetadata(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 Keychainretrieve(forKey:)- Get token from Keychaindelete(forKey:)- Remove tokenclearAll()- 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 responseAuthorizationServerMetadata- OAuth server configResourceServerMetadata- PDS OAuth configPARResponse- PAR request result
DIDDocument.swift#
Purpose: DID document structure
Extensions:
pdsEndpoint- Extract PDS URL from serviceshandle- Extract handle from alsoKnownAs
XRPCModels.swift#
Purpose: XRPC request/response structures
Models:
CreateRecordRequest- Post creation requestCreateRecordResponse- 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 <token> │
│ ├─ Add DPoP: <proof> │
│ └─ 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:
@Publishedproperties in ViewModel@EnvironmentObjectfor dependency injection@StateObjectfor ViewModel ownership@Statefor 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 errorsIdentityError- Resolution failuresKeychainError- Storage errorsXRPCError- 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 functionsDPoPGenerator- JWT generationIdentityResolver- With mock URLSessionKeychainManager- 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#
- Native Only: No external dependencies for maintainability
- MVVM: Clear separation of concerns
- Keychain: Secure by default
- ASWebAuthenticationSession: Standard, secure, maintains cookies
- SwiftUI: Modern, declarative, type-safe
- async/await: Modern concurrency, easier to read
- 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.