This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

docs for mobile with capacitor

+517
+54
.agent/logs/20260402_atproto_oauth_docs.md
··· 1 + Localhost Client Development 2 + 3 + When working with a development environment (Authorization Server and Client), it may be difficult for developers to publish in-progress client metadata at a public URL so that authorization servers can access it. This may even be true for development environments using a containerized Authorization Server and local DNS, because of SSRF protections against local IP ranges. 4 + 5 + To make development workflows easier, a special exception is made for clients with client_id having origin http://localhost (with no port number specified). Authorization Servers are encouraged to support this exception - including in production environments - but it is optional. 6 + 7 + In a localhost client_id scenario, the Authorization Server should verify that the scheme is http, and that the hostname is exactly localhost with no port specified. IP addresses (127.0.0.1, etc) are not supported. The path parameter must be empty (/). 8 + 9 + In the Authorization Request, the redirect_uri must match one of those supplied (or a default). Path components must match, but port numbers are not matched. 10 + 11 + Some metadata fields can be configured via query parameter in the client_id URL (with appropriate URL encoding): 12 + 13 + redirect_uri (string, multiple query parameters allowed, optional): allows declaring a local redirect/callback URL, with path component matched but port numbers ignored. The default values (if none are supplied) are http://127.0.0.1/ and http://[::1]/. 14 + scope (string with space-separated values, single query parameter allowed, optional): the set of scopes which might be requested by the client. Default is atproto. 15 + 16 + The other parameters in the virtual client metadata document will be: 17 + 18 + client_id (string): the exact client_id (URL) used to generate the virtual document 19 + client_name (string): a value chosen by the Authorization Server (e.g. "Development client") 20 + response_types (array of strings): must include code 21 + grant_types (array of strings): authorization_code and refresh_token 22 + token_endpoint_auth_method: none 23 + application_type: native 24 + dpop_bound_access_tokens: true 25 + 26 + Note that this works as a public client, not a confidential client. 27 + Identity Authentication 28 + 29 + As mentioned in the introduction, OAuth 2.0 generally provides only Authorization (authz), and additional standards like OpenID/OIDC are used for Authentication (authn). The atproto profile of OAuth requires authentication of account identity and supports the use case of simple identity authentication without additional resource access authorization. 30 + 31 + In atproto, account identity is anchored in the account DID, which is the permanent, globally unique, publicly resolvable identifier for the account. The DID resolves to a DID document which indicates the current PDS host location for the account. That PDS (combined with an optional entryway) is the authorization authority and the OAuth Authorization Server for the account. When speaking to any Authorization Server, it is critical (mandatory) for clients to confirm that it is actually the authoritative server for the account in question, which means independently resolving the account identity (by DID) and confirming that the Authorization Server matches. It is also critical (mandatory) to confirm at the end of an authorization flow that the Authorization Server actually authorized the expected account. The reason this is necessary is to confirm that the Authorization Server is authoritative for the account in question. Otherwise a malicious server could authenticate arbitrary accounts (DIDs) to the client. 32 + 33 + Clients can start an auth flow in one of two ways: 34 + 35 + starting with a public account identifier, provided by the user: handle or DID 36 + starting with a server hostname, provided by the user: PDS or entryway, mapping to either Resource Server and/or Authorization Server 37 + 38 + One use case for starting with a server instead of an account identifier is when the user does not remember their full account handle or only knows their account email. Another is for authentication when a user’s handle is broken. The user will still need to know their hosting provider in these situations. 39 + 40 + When starting with an account identifier, the client must resolve the atproto identity to a DID document. If starting with a handle, it is critical (mandatory) to bidirectionally verify the handle by checking that the DID document claims the handle (see atproto Handle specification). All handle resolution techniques and all atproto-blessed DID methods must be supported to ensure interoperability with all accounts. 41 + 42 + In some client environments, it may be difficult to resolve all identity types. For example, handle resolution may involve DNS TXT queries, which are not directly supported from browser apps. Client implementations might use alternative techniques (such as DNS-over-HTTP) or could make use of a supporting web service to resolve identities. 43 + 44 + Because authorization flows are security-critical, any caching of identity resolution should choose cache lifetimes carefully. Cache lifetimes of less than 10 minutes are recommended for auth flows specifically. 45 + 46 + The resolved DID should be bound to the overall auth session and should be used as the primary account identifier within client app code. Handles (when verified) are acceptable to display in user interfaces, but may change over time and need to be re-verified periodically. When passing an account identifier through to the Authorization Server as part of the Authorization Request in the login_hint, it is recommended to use the exact account identifier supplied by the user (handle or DID) to ensure any sign-in flow is consistent (users might not recognize their own account DID). 47 + 48 + At the end of the auth flow, when the client does an initial token fetch, the Authorization Server must return the account DID in the sub field of the JSON response body. If the entire auth flow started with an account identifier, it is critical for the client to verify that this DID matches the expected DID bound to the session earlier; the linkage from account to Authorization Server will already have been verified in this situation. 49 + 50 + If the auth flow instead starts with a server (hostname or URL), the client will first attempt to fetch Resource Server metadata (and resolve to Authorization Server if found) and then attempt to fetch Authorization Server metadata. See "Authorization Server" section for server metadata fetching. If either is successful, the client will end up with an identified Authorization Server. The Authorization Request flow will proceed without a login_hint or account identifier being bound to the session, but the Authorization Server issuer will be bound to the session. 51 + 52 + After the auth flow continues and an initial token request succeeds, the client will parse the account identifier from the sub field in the token response. At this point, the client still cannot trust that it has actually authenticated the indicated account. It is critical for the client to resolve the identity (DID document), extract the declared PDS host, confirm that the PDS (Resource Server) resolves to the Authorization Server bound to the session by fetching the Resource Server metadata, and fetch the Authorization Server metadata to confirm that the issuer field matches the Authorization Server origin (see draft-ietf-oauth-v2-1 section 7.3.1 regarding this last point). 53 + 54 + To reiterate, it is critical for all clients - including those only interested in atproto Identity Authentication - to go through the entire Authorization flow and to verify that the account identifier (DID) in the sub field of the token response is consistent with the Authorization Server hostname/origin (issuer).
+463
.agent/logs/20260402_semble_mobile_capacitor.md
··· 1 + # Semble Mobile App with Capacitor - Complete Implementation Guide 2 + 3 + ## Table of Contents 4 + 5 + 1. [Executive Summary](#executive-summary) 6 + 2. [File Structure & Relevant Paths](#file-structure--relevant-paths) 7 + 3. [OAuth Flow - Web vs Mobile](#oauth-flow---web-vs-mobile) 8 + 4. [Authentication Token Management](#authentication-token-management) 9 + 5. [Implementation Status](#implementation-status) 10 + 6. [Next Steps](#next-steps) 11 + 12 + --- 13 + 14 + ## Executive Summary 15 + 16 + Semble is a Progressive Web App (PWA) built with Next.js that's being wrapped with Capacitor to create native iOS and Android apps. The application uses ATProto OAuth for authentication with Bluesky accounts. 17 + 18 + ### Key Platform Differences 19 + 20 + | Aspect | Web | Mobile (Capacitor) | 21 + | ---------------------- | -------------------------------- | ------------------------------------------------- | 22 + | **OAuth Redirect** | Browser redirect to callback URL | Deep link to `so.semble://oauth-callback` | 23 + | **Browser** | Same window navigation | In-app browser (`@capacitor/browser`) | 24 + | **Token Storage** | httpOnly cookies | Needs native secure storage (not yet implemented) | 25 + | **Platform Detection** | N/A | Uses `@capacitor/core` to detect native platform | 26 + | **App ID** | N/A | `so.semble` | 27 + 28 + ### Current Mobile Configuration 29 + 30 + - **App ID:** `so.semble` 31 + - **Deep Link Scheme:** `so.semble://` 32 + - **Development Server:** `http://127.0.0.1:4000` 33 + - **Production Server:** `https://semble.so` (needs configuration) 34 + 35 + --- 36 + 37 + ## File Structure & Relevant Paths 38 + 39 + ### Frontend Files (Mobile-Specific) 40 + 41 + #### Capacitor Configuration 42 + 43 + - `src/webapp/capacitor.config.ts` - Main Capacitor configuration 44 + - `src/webapp/android/app/src/main/AndroidManifest.xml` - Android deep link configuration 45 + - `src/webapp/ios/App/App/Info.plist` - iOS URL scheme configuration 46 + 47 + #### OAuth & Authentication Components 48 + 49 + - `src/webapp/features/auth/components/loginForm/LoginForm.tsx` - Main login form with platform detection 50 + - `src/webapp/features/auth/components/loginForm/OAuthLoginForm.tsx` - OAuth-specific login UI 51 + - `src/webapp/lib/auth/oauthDeepLinkHandler.ts` - **Mobile-only:** Handles OAuth deep link callbacks 52 + - `src/webapp/lib/capacitor/platform.ts` - Platform detection utilities 53 + 54 + #### API Client 55 + 56 + - `src/webapp/api-client/clients/UserClient.ts` - User authentication API client 57 + - `src/webapp/api-client/clients/BaseClient.ts` - Base HTTP client (needs modification for mobile tokens) 58 + 59 + #### Authentication Hooks & Services 60 + 61 + - `src/webapp/hooks/useAuth.tsx` - Main auth hook 62 + - `src/webapp/services/auth/CookieAuthService.client.ts` - Client-side cookie service 63 + - `src/webapp/services/auth/CookieAuthService.server.ts` - Server-side cookie service 64 + - `src/webapp/services/client.apiClient.ts` - API client factory 65 + 66 + #### Providers 67 + 68 + - `src/webapp/providers/index.tsx` - Initializes OAuth deep link handler for mobile 69 + 70 + ### Backend Files 71 + 72 + #### OAuth Controllers 73 + 74 + - `src/modules/user/infrastructure/http/controllers/InitiateOAuthSignInController.ts` - Starts OAuth flow 75 + - `src/modules/user/infrastructure/http/controllers/CompleteOAuthSignInController.ts` - Completes OAuth, sets cookies/returns tokens 76 + 77 + #### OAuth Use Cases 78 + 79 + - `src/modules/user/application/use-cases/InitiateOAuthSignInUseCase.ts` - Business logic for OAuth initiation 80 + - `src/modules/user/application/use-cases/CompleteOAuthSignInUseCase.ts` - Business logic for OAuth completion 81 + 82 + #### OAuth Services & Infrastructure 83 + 84 + - `src/modules/user/infrastructure/services/OAuthClientFactory.ts` - **Key file:** Creates platform-specific OAuth clients 85 + - `src/modules/atproto/infrastructure/services/AtProtoOAuthProcessor.ts` - ATProto OAuth implementation 86 + - `src/shared/infrastructure/http/factories/ServiceFactory.ts` - Service initialization (accepts platform parameter) 87 + - `src/shared/infrastructure/http/services/CookieService.ts` - Cookie management for web 88 + 89 + #### Type Definitions 90 + 91 + - `src/types/src/api/requests.ts` - API request types (includes `client` field for platform detection) 92 + - `src/types/src/api/responses.ts` - API response types 93 + - `src/types/src/api/internal.ts` - Internal types (TokenPair) 94 + 95 + --- 96 + 97 + ## OAuth Flow - Web vs Mobile 98 + 99 + ### Web OAuth Flow (Production) 100 + 101 + ``` 102 + ┌──────────┐ ┌──────────────┐ ┌──────────────┐ 103 + │ │ 1. GET /api/users │ │ 2. Generate │ │ 104 + │ Browser │ /login?handle= │ Backend │ auth URL │ Bluesky PDS │ 105 + │ │ ─────────────────> │ │ ──────────────────> │ │ 106 + │ │ │ │ │ │ 107 + │ │ 3. authUrl │ │ │ │ 108 + │ │ <───────────────── │ │ │ │ 109 + │ │ │ │ │ │ 110 + │ │ 4. window.location.href = authUrl │ │ 111 + │ │ ───────────────────────────────────────────────────────>│ │ 112 + │ │ │ │ │ │ 113 + │ │ 5. User authorizes │ │ 114 + │ │ │ │ │ │ 115 + │ │ 6. Redirect to │ │ 7. GET /api/users │ │ 116 + │ │ callback URL │ │ /oauth/callback│ │ 117 + │ │ <─────────────────── │ │ <───────────────── │ │ 118 + │ │ │ │ ?code=&state= │ │ 119 + │ │ │ │ │ │ 120 + │ │ 8. Set httpOnly │ │ 9. Exchange code │ │ 121 + │ │ cookies + │ │ for tokens │ │ 122 + │ │ redirect /home │ │ ──────────────────> │ │ 123 + │ │ <─────────────────── │ │ <───────────────── │ │ 124 + └──────────┘ └──────────────┘ └──────────────┘ 125 + 126 + OAuth Client Configuration (Production): 127 + - client_id: https://semble.so/atproto/oauth-client-metadata.json 128 + - redirect_uri: https://semble.so/api/users/oauth/callback 129 + - application_type: web 130 + ``` 131 + 132 + ### Mobile OAuth Flow (Production) 133 + 134 + ``` 135 + ┌──────────┐ ┌──────────────┐ ┌──────────────┐ 136 + │ Mobile │ 1. GET /api/users │ │ 2. Generate │ │ 137 + │ App │ /login?handle= │ Backend │ auth URL │ Bluesky PDS │ 138 + │ │ &client=native │ │ ──────────────────> │ │ 139 + │ │ ─────────────────> │ │ │ │ 140 + │ │ │ │ │ │ 141 + │ │ 3. authUrl │ │ │ │ 142 + │ │ <───────────────── │ │ │ │ 143 + │ │ │ │ │ │ 144 + │ │ 4. Browser.open({url: authUrl}) │ │ 145 + │ │ ────────────────────────────────────────────────────────> │ 146 + │ │ (Opens in-app browser) │ │ │ 147 + │ │ │ │ │ │ 148 + │ │ 5. User authorizes in browser │ │ │ 149 + │ │ │ │ │ │ 150 + │ │ 6. Deep link: │ │ │ │ 151 + │ │ so.semble:// │ │ │ │ 152 + │ │ oauth-callback │ │ │ │ 153 + │ │ <─────────────────── │ │ │ │ 154 + │ │ ?code=&state=&iss= │ │ │ │ 155 + │ │ │ │ │ │ 156 + │ │ 7. App.addListener │ │ │ │ 157 + │ │ catches deep link│ │ │ │ 158 + │ │ │ │ │ │ 159 + │ │ 8. Browser.close() │ │ │ │ 160 + │ │ │ │ │ │ 161 + │ │ 9. GET /api/users │ │ 10. Exchange code │ │ 162 + │ │ /oauth/callback │ │ for tokens │ │ 163 + │ │ ?client=native │ │ ──────────────────> │ │ 164 + │ │ ─────────────────> │ │ <───────────────── │ │ 165 + │ │ │ │ │ │ 166 + │ │ 11. JSON response │ │ │ │ 167 + │ │ (not redirect) │ │ │ │ 168 + │ │ <─────────────────── │ │ │ │ 169 + │ │ │ │ │ │ 170 + │ │ 12. Store tokens │ │ │ │ 171 + │ │ (TODO: secure │ │ │ │ 172 + │ │ storage) │ │ │ │ 173 + │ │ │ │ │ │ 174 + │ │ 13. Router.push │ │ │ │ 175 + │ │ ('/home') │ │ │ │ 176 + └──────────┘ └──────────────┘ └──────────────┘ 177 + 178 + OAuth Client Configuration (Production): 179 + - client_id: https://semble.so/atproto/oauth-client-metadata.json 180 + - redirect_uri: so.semble://oauth-callback 181 + - application_type: native 182 + ``` 183 + 184 + ### Local Development OAuth Flow 185 + 186 + For local development, ATProto OAuth supports a special `localhost` exception: 187 + 188 + ``` 189 + OAuth Client Configuration (Local Development): 190 + - client_id: http://localhost?redirect_uri=<encoded_uri>&scope=<encoded_scope> 191 + - Web redirect_uri: http://127.0.0.1:3000/api/users/oauth/callback 192 + - Mobile redirect_uri: so.semble://oauth-callback 193 + - application_type: web (for web) / native (for mobile) 194 + ``` 195 + 196 + This allows development without publishing client metadata. 197 + 198 + --- 199 + 200 + ## Authentication Token Management 201 + 202 + ### Current Web Implementation (Cookie-Based) 203 + 204 + **File:** `src/shared/infrastructure/http/services/CookieService.ts` 205 + 206 + ```typescript 207 + // Cookie configuration for web 208 + setTokens(res: Response, tokens: { accessToken: string; refreshToken: string }) { 209 + const cookieOptions = { 210 + httpOnly: true, // Prevents XSS attacks 211 + secure: isProd, // HTTPS only in production 212 + sameSite: 'lax', // CSRF protection 213 + domain: '.semble.so', // Production domain 214 + maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days 215 + }; 216 + 217 + res.cookie('accessToken', tokens.accessToken, cookieOptions); 218 + res.cookie('refreshToken', tokens.refreshToken, cookieOptions); 219 + } 220 + ``` 221 + 222 + **Web Token Flow:** 223 + 224 + 1. Login sets httpOnly cookies on backend 225 + 2. Browser automatically includes cookies with `credentials: 'include'` 226 + 3. Backend validates cookies on each request 227 + 4. Auto-refresh handled by `/api/auth/me` endpoint 228 + 229 + ### Mobile Token Management Issues 230 + 231 + **Problem:** httpOnly cookies don't persist reliably in Capacitor WebViews 232 + 233 + ``` 234 + ┌─────────────────────────────────────────────┐ 235 + │ iOS Cookie Persistence │ 236 + ├─────────────────────────────────────────────┤ 237 + │ App Launch → Cookies exist ✓ │ 238 + │ API calls work ✓ │ 239 + │ App goes to background... │ 240 + │ App terminated by OS... │ 241 + │ User relaunches app → Cookies lost ✗ │ 242 + │ User must login again ✗ │ 243 + └─────────────────────────────────────────────┘ 244 + ``` 245 + 246 + ### Recommended Mobile Token Storage Solution 247 + 248 + **Use Native Secure Storage (not yet implemented):** 249 + 250 + ```typescript 251 + // Proposed NativeTokenService.ts 252 + import { SecureStorage } from '@capacitor-community/secure-storage'; 253 + 254 + class NativeTokenService { 255 + async storeTokens(tokens: { accessToken: string; refreshToken: string }) { 256 + // Store in iOS Keychain / Android Keystore 257 + await SecureStorage.set({ key: 'accessToken', value: tokens.accessToken }); 258 + await SecureStorage.set({ 259 + key: 'refreshToken', 260 + value: tokens.refreshToken, 261 + }); 262 + } 263 + 264 + async getAccessToken(): Promise<string | null> { 265 + try { 266 + const { value } = await SecureStorage.get({ key: 'accessToken' }); 267 + return value; 268 + } catch { 269 + return null; 270 + } 271 + } 272 + } 273 + ``` 274 + 275 + **Modified API Client for Mobile:** 276 + 277 + ```typescript 278 + // BaseClient.ts modification needed 279 + async request(method: string, endpoint: string, data?: any) { 280 + const headers: any = { 'Content-Type': 'application/json' }; 281 + 282 + if (isNativePlatform()) { 283 + // Mobile: Add token to Authorization header 284 + const token = await NativeTokenService.getAccessToken(); 285 + if (token) { 286 + headers['Authorization'] = `Bearer ${token}`; 287 + } 288 + } 289 + 290 + // Web: Use credentials: 'include' for cookies 291 + const options = { 292 + method, 293 + headers, 294 + body: data ? JSON.stringify(data) : undefined, 295 + credentials: isNativePlatform() ? 'omit' : 'include' 296 + }; 297 + 298 + return fetch(endpoint, options); 299 + } 300 + ``` 301 + 302 + ### Platform-Specific Token Storage Comparison 303 + 304 + | Platform | Storage Method | Security | Persistence | Access | 305 + | ------------------------- | ---------------- | ------------------ | ------------------ | ----------- | 306 + | **Web** | httpOnly Cookies | ✅ XSS-proof | ✅ Excellent | Server-only | 307 + | **iOS (Current)** | httpOnly Cookies | ⚠️ Variable | ❌ Lost on restart | Server-only | 308 + | **iOS (Recommended)** | Keychain | ✅ Hardware-backed | ✅ Excellent | Native API | 309 + | **Android (Current)** | httpOnly Cookies | ⚠️ Variable | ⚠️ Usually works | Server-only | 310 + | **Android (Recommended)** | Keystore | ✅ Hardware-backed | ✅ Excellent | Native API | 311 + 312 + --- 313 + 314 + ## Implementation Status 315 + 316 + ### ✅ Completed 317 + 318 + #### Deep Link Configuration 319 + 320 + - **Android:** Intent filter added to `AndroidManifest.xml` for `so.semble://oauth-callback` 321 + - **iOS:** URL scheme `so.semble` added to `Info.plist` 322 + 323 + #### OAuth Client Factory 324 + 325 + - `OAuthClientFactory.ts` now accepts `isNativePlatform` parameter 326 + - Returns platform-specific configuration: 327 + - Native: `redirect_uri: so.semble://oauth-callback`, `application_type: native` 328 + - Web: `redirect_uri: https://semble.so/api/users/oauth/callback`, `application_type: web` 329 + 330 + #### OAuth Flow 331 + 332 + - Frontend detects platform and uses in-app browser for mobile 333 + - Deep link handler (`oauthDeepLinkHandler.ts`) catches OAuth callback 334 + - Backend supports `client=native` parameter for mobile-specific responses 335 + 336 + #### Type Definitions 337 + 338 + - Updated `InitiateOAuthSignInRequest` and `CompleteOAuthSignInRequest` to include optional `client` field 339 + 340 + ### ❌ Not Yet Implemented 341 + 342 + #### Token Storage for Mobile 343 + 344 + 1. **Secure Storage Plugin:** Need to install `@capacitor-community/secure-storage` 345 + 2. **Token Service:** Need to create `NativeTokenService.ts` for secure storage operations 346 + 3. **API Client Updates:** `BaseClient.ts` needs to read tokens from secure storage on mobile 347 + 4. **Backend Response:** `CompleteOAuthSignInController` needs to return tokens in response body for native clients (currently only returns success message) 348 + 5. **Token Refresh:** Mobile-specific refresh logic (can't rely on cookie-based refresh) 349 + 6. **Logout:** Clear tokens from secure storage on mobile logout 350 + 351 + #### Production Configuration 352 + 353 + 1. **Server URL:** Update `capacitor.config.ts` from `http://127.0.0.1:4000` to `https://semble.so` 354 + 2. **Cleartext:** Disable `cleartext: true` in production 355 + 3. **OAuth Metadata:** Ensure OAuth client metadata is properly hosted 356 + 357 + --- 358 + 359 + ## Next Steps 360 + 361 + ### Priority 1: Enable Mobile Token Persistence (Required for Launch) 362 + 363 + 1. **Install Secure Storage Plugin:** 364 + 365 + ```bash 366 + npm install @capacitor-community/secure-storage 367 + npx cap sync 368 + ``` 369 + 370 + 2. **Create Token Service:** 371 + - Create `src/webapp/services/auth/NativeTokenService.ts` 372 + - Create `src/webapp/services/auth/TokenService.ts` (platform-aware facade) 373 + 374 + 3. **Update Backend:** 375 + 376 + ```typescript 377 + // CompleteOAuthSignInController.ts line 48-52 378 + if (client === 'native') { 379 + return this.ok(res, { 380 + message: 'OAuth sign-in completed successfully', 381 + accessToken: result.value.accessToken, // ADD THIS 382 + refreshToken: result.value.refreshToken, // ADD THIS 383 + }); 384 + } 385 + ``` 386 + 387 + 4. **Update Deep Link Handler:** 388 + 389 + ```typescript 390 + // oauthDeepLinkHandler.ts after line 98 391 + const response = await client.completeOAuthSignIn({...}); 392 + 393 + // Store tokens in secure storage 394 + if (response.accessToken && response.refreshToken) { 395 + await NativeTokenService.storeTokens({ 396 + accessToken: response.accessToken, 397 + refreshToken: response.refreshToken 398 + }); 399 + } 400 + ``` 401 + 402 + 5. **Update API Client:** 403 + - Modify `BaseClient.ts` to add Authorization header on mobile 404 + - Keep `credentials: 'include'` for web 405 + 406 + ### Priority 2: Production Configuration 407 + 408 + 1. Update `capacitor.config.ts`: 409 + 410 + ```typescript 411 + server: { 412 + url: process.env.NODE_ENV === 'production' 413 + ? 'https://semble.so' 414 + : 'http://127.0.0.1:4000', 415 + cleartext: process.env.NODE_ENV !== 'production' 416 + } 417 + ``` 418 + 419 + 2. Test OAuth flow in production environment 420 + 421 + ### Priority 3: Enhanced Security & UX 422 + 423 + 1. Implement token refresh for mobile 424 + 2. Add biometric authentication option 425 + 3. Implement secure token rotation 426 + 4. Add offline support considerations 427 + 428 + --- 429 + 430 + ## Testing Checklist 431 + 432 + ### Web Platform 433 + 434 + - [ ] OAuth login works with cookies 435 + - [ ] Tokens persist across browser sessions 436 + - [ ] Auto-refresh works when token expires 437 + - [ ] Logout clears cookies 438 + 439 + ### Mobile Platform (iOS) 440 + 441 + - [ ] Deep link opens app from OAuth callback 442 + - [ ] Tokens stored in Keychain 443 + - [ ] Tokens persist after app restart 444 + - [ ] API calls include Authorization header 445 + - [ ] Logout clears Keychain 446 + 447 + ### Mobile Platform (Android) 448 + 449 + - [ ] Deep link opens app from OAuth callback 450 + - [ ] Tokens stored in Keystore 451 + - [ ] Tokens persist after app restart 452 + - [ ] API calls include Authorization header 453 + - [ ] Logout clears Keystore 454 + 455 + --- 456 + 457 + ## Additional Resources 458 + 459 + - [ATProto OAuth Specification](https://atproto.com/specs/oauth) 460 + - [Capacitor Documentation](https://capacitorjs.com/docs) 461 + - [Capacitor Secure Storage Plugin](https://github.com/capacitor-community/secure-storage) 462 + - [iOS Keychain Services](https://developer.apple.com/documentation/security/keychain_services) 463 + - [Android Keystore](https://developer.android.com/training/articles/keystore)