- Updated
@tijs/atproto-oauth-honofrom 0.3.1 to 0.4.0 - Created
backend/services/auth.tswith kipclip-style auth helpers:getAuthSession(req)- Cookie-based authentication with automatic token refreshclearSessionCookie()- Generate cookie clear headerunauthorizedResponse(c)- Consistent 401 response
- Updated
createCheckin()anddeleteCheckin()to use new helpers - Removed old
authenticateUser()function
- Created
frontend/utils/api.tswith automatic 401 handling - All authenticated requests use
apiFetch()wrapper - Automatic redirect to login on session expiry
- No more confusing "Authentication required" errors
- All 97 tests passing
- iOS app is temporarily broken - cannot authenticate with current backend
- Old Bearer token flow removed from backend
- iOS needs to be updated to use new session validation pattern
Implement proper session validation in the iOS app following the kipclip pattern, adapted for native iOS architecture.
/api/auth/sessionendpoint exists (provided by atproto-oauth-hono)- Automatically refreshes expired tokens via
oauth.sessions.getOAuthSession() - Returns session data:
{ valid, did, handle, displayName, avatar, accessToken, refreshToken, expiresAt } - Supports both cookie (web) and Bearer token (mobile) authentication
File: AnchorKit/Sources/AnchorKit/Services/AnchorAuthService.swift
Changes:
- Implement real
validateSession()that calls/api/auth/sessionendpoint - Add
validateSessionWithBackend()method for explicit validation - Parse response and update credentials with new token data
- Handle 401 responses as session expired
Example Implementation:
public func validateSession(_ credentials: AuthCredentials) async throws -> AuthCredentials { let url = URL(string: "https://dropanchor.app/api/auth/session")! var request = URLRequest(url: url) request.setValue("Bearer \(credentials.sessionId ?? "")", forHTTPHeaderField: "Authorization") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw AnchorAuthError.invalidResponse } if httpResponse.statusCode == 401 { throw AnchorAuthError.sessionExpired } let sessionData = try JSONDecoder().decode(SessionResponse.self, from: data) if !sessionData.valid { throw AnchorAuthError.sessionExpired } // Update credentials with new expiration var updatedCredentials = credentials updatedCredentials.expiresAt = Date(timeIntervalSince1970: sessionData.expiresAt) return updatedCredentials }
File: AnchorKit/Sources/AnchorKit/Stores/AuthStore.swift
Changes:
-
validateSessionOnAppLaunch():- Call backend validation using updated
AnchorAuthService - If validation fails → attempt token refresh
- If refresh fails → sign out user and clear credentials
- Update authentication state
- Call backend validation using updated
-
validateSessionOnAppResume():- Call lightweight session validation
- Handle expired sessions gracefully
- Update UI state to show login prompt
Example Implementation:
public func validateSessionOnAppLaunch() async { guard let credentials = _credentials else { return } do { let validatedCredentials = try await authService.validateSession(credentials) _credentials = validatedCredentials await credentialsStorage.save(validatedCredentials) } catch { // Validation failed, try refresh do { try await refreshSession() } catch { // Refresh failed, sign out await signOut() } } }
File: AnchorKit/Sources/AnchorKit/Stores/CheckInStore.swift
Changes:
- Add
requireValidSession()helper method - Call before creating checkin
- Throw clear
SessionExpiredErrorif validation fails - UI layer catches this and shows "Sign in again" prompt
Example Implementation:
private func requireValidSession() async throws { guard let credentials = try await authStore.getValidCredentials() else { throw CheckInError.sessionExpired } } public func createCheckin(...) async throws { try await requireValidSession() // ... rest of checkin creation logic }
File: AnchorMobile/Views/CheckInComposeView.swift
Changes:
- Catch
SessionExpiredErrorspecifically - Show alert: "Your session has expired. Please sign in again."
- Provide "Sign In" button that navigates to SettingsView
- Clear any in-progress checkin data
Example Implementation:
.alert("Session Expired", isPresented: $showSessionExpiredAlert) { Button("Sign In") { // Navigate to settings for re-authentication selectedTab = .settings } Button("Cancel", role: .cancel) { } } message: { Text("Your session has expired. Please sign in again to continue.") }
File: AnchorMobile/Views/SettingsView.swift
Changes:
- Display actual session validity (not just
isAuthenticated) - Show expiration time if available
- Add visual indicator for expired sessions
- Make re-authentication easy with clear CTA
File: AnchorKit/Services/Auth/IronSessionAPIClient.swift
Changes:
- Detect 401 responses in API client
- Automatically clear credentials on persistent 401s
- Update AuthStore state to trigger login UI
- Prevent cascading authentication errors
- Mock
/api/auth/sessionendpoint responses - Test
validateSession()with valid/expired/invalid responses - Test
validateSessionOnAppLaunch()with various session states - Test pre-operation validation in CheckInStore
- Test error handling flows
-
Expired Session on Launch:
- Launch app with expired session in Keychain
- Verify session validation is attempted
- Verify user is signed out if validation fails
-
Expired Session During Use:
- App in use, session expires in background
- User attempts to create checkin
- Verify clear error message and re-auth prompt
-
Token Refresh Success:
- Session near expiry
- Validation triggers automatic refresh
- User continues without interruption
-
Token Refresh Failure:
- Session expired and refresh tokens invalid
- User is signed out gracefully
- Clear path to re-authenticate
// Session response from /api/auth/session struct SessionResponse: Codable { let valid: Bool let did: String? let handle: String? let displayName: String? let avatar: String? let accessToken: String? let refreshToken: String? let expiresAt: TimeInterval? } // New error types enum SessionError: Error { case expired case invalid case networkFailure case refreshFailed } enum CheckInError: Error { case sessionExpired case invalidData case networkFailure }
- App loads credentials from Keychain
- Calls
/api/auth/sessionto validate - Backend returns 401 (session expired)
- App clears credentials from Keychain
- Shows login screen
- ✅ No confusing error messages
- User fills out checkin form
- Taps "Drop Anchor"
- Pre-operation validation detects expired session
- Shows "Session expired - please sign in again" alert
- User taps "Sign In" button
- Navigates to Settings view for re-authentication
- ✅ Clear recovery path
- App goes to background
- Session expires (30 days later)
- User brings app to foreground
validateSessionOnAppResume()detects expiration- Signs out user, shows login screen
- ✅ Proactive session management
✅ App validates session on launch and resume using backend endpoint ✅ Expired sessions detected before user attempts actions ✅ Clear "Sign in again" messaging when session expires ✅ No confusing "Authentication required" errors ✅ Consistent auth state across the app ✅ Smooth re-authentication flow ✅ Backend and iOS app fully aligned with kipclip pattern
AnchorKit (Business Logic):
Services/AnchorAuthService.swift- Real session validationStores/AuthStore.swift- Lifecycle validationStores/CheckInStore.swift- Pre-operation validationServices/Auth/IronSessionAPIClient.swift- Centralized 401 handling (optional)
AnchorMobile (UI Layer):
Views/CheckInComposeView.swift- Error handlingViews/SettingsView.swift- Session status displayViews/Authentication/*- Re-auth flow improvements
Tests:
AnchorKit/Tests/Services/AnchorAuthServiceTests.swiftAnchorKit/Tests/Stores/AuthStoreTests.swiftAnchorKit/Tests/Stores/CheckInStoreTests.swift
- iOS app currently broken until Phase 2 implementation
- Web app fully functional with new auth flow
- Backend ready for iOS -
/api/auth/sessionendpoint works - Pattern matches kipclip-appview (adapted for iOS native)
- No backward compatibility needed - clean slate for new pattern
- Step 1 (Auth Service): ~2 hours
- Step 2 (AuthStore): ~2 hours
- Step 3 (CheckInStore): ~1 hour
- Step 4 (UI Error Handling): ~2 hours
- Step 5 (Settings View): ~1 hour
- Step 6 (Optional 401 handling): ~1 hour
- Testing: ~3 hours
Total: ~12 hours of development work
- kipclip-appview auth pattern:
/Users/tijs/projects/atproto/kipclip.com/kipclip-appview/backend/services/auth.ts - Anchor web frontend:
/Users/tijs/projects/atproto/anchor-appview/frontend/utils/api.ts - Current iOS implementation:
/Users/tijs/projects/atproto/Anchor/Anchor/AnchorKit/