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:

@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:

{
  "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 <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:

  • @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.