|
| 1 | +# OAuth Authentication Implementation Plan |
| 2 | + |
| 3 | +This document outlines the phased implementation plan for replacing API key authentication with OAuth in the Array app. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +- **Goal**: Replace manual API key entry with "Sign in with PostHog" OAuth flow |
| 8 | +- **Strategy**: OAuth only (remove API key option) |
| 9 | +- **Callback**: Localhost HTTP server on port 8239 (same as CLI) |
| 10 | +- **Regions**: Support US/EU/Dev with region selector |
| 11 | +- **Token Refresh**: Proactive background refresh + reactive 401 handling + refresh token rotation |
| 12 | + |
| 13 | +## Stage 1: Core OAuth Infrastructure |
| 14 | + |
| 15 | +**PR 1: Add OAuth service and IPC handlers** |
| 16 | + |
| 17 | +### Files to Create: |
| 18 | +- `src/shared/constants/oauth.ts` - OAuth constants (client IDs, port, scopes) |
| 19 | +- `src/shared/types/oauth.ts` - TypeScript types (CloudRegion, OAuthTokenResponse, etc.) |
| 20 | +- `src/main/services/oauth.ts` - OAuth service with PKCE flow |
| 21 | + |
| 22 | +### Files to Modify: |
| 23 | +- `src/main/index.ts` - Register OAuth IPC handlers |
| 24 | +- `src/main/preload.ts` - Add OAuth IPC type definitions |
| 25 | + |
| 26 | +### Implementation Details: |
| 27 | +1. **OAuth Constants** (`src/shared/constants/oauth.ts`) |
| 28 | + - Client IDs: `POSTHOG_US_CLIENT_ID`, `POSTHOG_EU_CLIENT_ID`, `POSTHOG_DEV_CLIENT_ID` |
| 29 | + - `OAUTH_PORT = 8239` |
| 30 | + - OAuth scopes array |
| 31 | + - Helper functions: `getCloudUrlFromRegion()`, `getOauthClientIdFromRegion()` |
| 32 | + |
| 33 | +2. **OAuth Types** (`src/shared/types/oauth.ts`) |
| 34 | + - `CloudRegion`: 'us' | 'eu' | 'dev' |
| 35 | + - `OAuthTokenResponse`: access_token, refresh_token, expires_in, etc. |
| 36 | + - `OAuthConfig`: scopes, cloudRegion |
| 37 | + |
| 38 | +3. **OAuth Service** (`src/main/services/oauth.ts`) |
| 39 | + - PKCE functions: `generateCodeVerifier()`, `generateCodeChallenge()` |
| 40 | + - `startCallbackServer()` - HTTP server handling `/authorize` and `/callback` |
| 41 | + - `exchangeCodeForToken()` - Code exchange with PostHog |
| 42 | + - `performOAuthFlow()` - Main orchestration function |
| 43 | + - Token encryption using `safeStorage` |
| 44 | + - IPC handlers: `startOAuthFlow`, `storeOAuthTokens`, `retrieveOAuthTokens`, `deleteOAuthTokens`, `refreshOAuthToken` |
| 45 | + |
| 46 | +4. **IPC Registration** (`src/main/index.ts`) |
| 47 | + - Import OAuth handlers |
| 48 | + - Register on app initialization |
| 49 | + |
| 50 | +5. **Preload Types** (`src/main/preload.ts`) |
| 51 | + - Add OAuth methods to `electronAPI` interface |
| 52 | + - Type-safe IPC bridge for renderer |
| 53 | + |
| 54 | +### Testing: |
| 55 | +- Verify OAuth server starts on port 8239 |
| 56 | +- Test PKCE code generation |
| 57 | +- Verify token encryption/decryption |
| 58 | +- Test IPC communication between renderer and main |
| 59 | + |
| 60 | +--- |
| 61 | + |
| 62 | +## Stage 2: State Management & Token Refresh |
| 63 | + |
| 64 | +**PR 2: Refactor authStore and add token refresh logic** |
| 65 | + |
| 66 | +### Files to Modify: |
| 67 | +- `src/renderer/features/auth/stores/authStore.ts` - Refactor for OAuth state |
| 68 | +- `src/api/fetcher.ts` - Add OAuth token and 401 handling |
| 69 | +- `src/api/posthogClient.ts` - Update constructor for OAuth |
| 70 | + |
| 71 | +### Implementation Details: |
| 72 | + |
| 73 | +1. **AuthStore Refactor** (`authStore.ts`) |
| 74 | + - **Remove**: `apiKey`, `encryptedKey`, `openaiApiKey`, `encryptedOpenaiKey` |
| 75 | + - **Add**: |
| 76 | + - `oauthAccessToken: string | null` |
| 77 | + - `oauthRefreshToken: string | null` |
| 78 | + - `tokenExpiry: number | null` (Unix timestamp) |
| 79 | + - `cloudRegion: CloudRegion | null` |
| 80 | + - **Methods**: |
| 81 | + - `loginWithOAuth(region: CloudRegion)` - Calls IPC to start OAuth flow |
| 82 | + - `refreshAccessToken()` - Refresh token logic with rotation handling |
| 83 | + - `scheduleTokenRefresh()` - Proactive refresh 5 mins before expiry |
| 84 | + - `initializeOAuth()` - Restore session on app launch |
| 85 | + - `logout()` - Clear OAuth tokens |
| 86 | + |
| 87 | +2. **Token Refresh Logic**: |
| 88 | + - Proactive: `setTimeout` scheduled refresh (expires_in - 5min) |
| 89 | + - Reactive: 401 interceptor in fetcher |
| 90 | + - Handle refresh token rotation (update both tokens) |
| 91 | + - On refresh failure: logout and show AuthScreen |
| 92 | + |
| 93 | +3. **API Fetcher Update** (`fetcher.ts`) |
| 94 | + - Change `Authorization` header to use OAuth access token |
| 95 | + - Add response interceptor: |
| 96 | + ```typescript |
| 97 | + if (response.status === 401) { |
| 98 | + await authStore.getState().refreshAccessToken(); |
| 99 | + // Retry original request |
| 100 | + } |
| 101 | + ``` |
| 102 | + - Remove API key logic |
| 103 | + |
| 104 | +4. **PostHog Client Update** (`posthogClient.ts`) |
| 105 | + - Constructor takes `accessToken` and `apiHost` |
| 106 | + - Derive `apiHost` from `cloudRegion` |
| 107 | + |
| 108 | +### Testing: |
| 109 | +- Test token refresh on 401 response |
| 110 | +- Test proactive token refresh |
| 111 | +- Test refresh token rotation |
| 112 | +- Test logout clears all OAuth state |
| 113 | + |
| 114 | +--- |
| 115 | + |
| 116 | +## Stage 3: UI Updates |
| 117 | + |
| 118 | +**PR 3: Update AuthScreen and SettingsView for OAuth** |
| 119 | + |
| 120 | +### Files to Modify: |
| 121 | +- `src/renderer/features/auth/components/AuthScreen.tsx` - OAuth UI |
| 122 | +- `src/renderer/features/settings/components/SettingsView.tsx` - OAuth status display |
| 123 | +- `src/renderer/App.tsx` - Update auth initialization |
| 124 | + |
| 125 | +### Implementation Details: |
| 126 | + |
| 127 | +1. **AuthScreen Redesign** (`AuthScreen.tsx`) |
| 128 | + - **Add**: Region selector (RadioGroup with US/EU/Dev options) |
| 129 | + - **Replace**: API key form with "Sign in with PostHog" button |
| 130 | + - **Add**: Loading state ("Waiting for authorization...") |
| 131 | + - **Add**: Error state handling (timeout, access denied, etc.) |
| 132 | + - **Remove**: API key TextField, custom host TextField |
| 133 | + - Layout: Keep two-pane design, left side has region selector + button |
| 134 | + |
| 135 | +2. **SettingsView Update** (`SettingsView.tsx`) |
| 136 | + - **Show**: OAuth connection status |
| 137 | + - Region badge (US/EU/Dev) |
| 138 | + - Connected account email (if available from token) |
| 139 | + - **Add**: "Re-authenticate" button |
| 140 | + - **Update**: "Sign out" button (calls OAuth logout) |
| 141 | + - **Remove**: API key display/edit fields |
| 142 | + |
| 143 | +3. **App.tsx Update** |
| 144 | + - Call `authStore.initializeOAuth()` on mount |
| 145 | + - Handle token expiry edge cases |
| 146 | + |
| 147 | +### Testing: |
| 148 | +- Test OAuth flow from AuthScreen |
| 149 | +- Test region switching |
| 150 | +- Test error states (timeout, denial) |
| 151 | +- Test re-authentication from Settings |
| 152 | +- Test logout flow |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## Stage 4: Cleanup & Polish |
| 157 | + |
| 158 | +**PR 4: Remove API key code and final cleanup** |
| 159 | + |
| 160 | +### Files to Modify: |
| 161 | +- `src/main/services/posthog.ts` - Remove API key IPC handlers |
| 162 | +- Clean up any unused imports/types |
| 163 | + |
| 164 | +### Implementation Details: |
| 165 | + |
| 166 | +1. **Remove API Key Code**: |
| 167 | + - Delete `storeApiKey`, `retrieveApiKey` IPC handlers |
| 168 | + - Remove API key encryption logic (if not used elsewhere) |
| 169 | + - Clean up unused imports |
| 170 | + |
| 171 | +2. **Migration Considerations**: |
| 172 | + - Old encrypted API keys in localStorage will be orphaned (acceptable) |
| 173 | + - Users will need to re-authenticate with OAuth |
| 174 | + |
| 175 | +3. **Documentation**: |
| 176 | + - Update README if it mentions API keys |
| 177 | + - Add OAuth setup instructions |
| 178 | + |
| 179 | +### Testing: |
| 180 | +- Full end-to-end OAuth flow for all regions |
| 181 | +- Test app launch with existing OAuth session |
| 182 | +- Test app launch without session (shows AuthScreen) |
| 183 | +- Test token refresh scenarios |
| 184 | +- Test logout and re-authentication |
| 185 | + |
| 186 | +--- |
| 187 | + |
| 188 | +## OAuth Flow Sequence |
| 189 | + |
| 190 | +``` |
| 191 | +1. User selects region (US/EU/Dev) |
| 192 | + ↓ |
| 193 | +2. User clicks "Sign in with PostHog" |
| 194 | + ↓ |
| 195 | +3. Renderer calls IPC: startOAuthFlow(region) |
| 196 | + ↓ |
| 197 | +4. Main process starts HTTP server on localhost:8239 |
| 198 | + ↓ |
| 199 | +5. Main process opens browser to localhost:8239/authorize |
| 200 | + ↓ |
| 201 | +6. Browser redirects to PostHog OAuth page (with PKCE challenge) |
| 202 | + ↓ |
| 203 | +7. User authorizes on PostHog |
| 204 | + ↓ |
| 205 | +8. PostHog redirects to localhost:8239/callback?code=... |
| 206 | + ↓ |
| 207 | +9. Callback server receives code |
| 208 | + ↓ |
| 209 | +10. Main process exchanges code for tokens |
| 210 | + ↓ |
| 211 | +11. Main process encrypts tokens |
| 212 | + ↓ |
| 213 | +12. Main process returns tokens to renderer via IPC |
| 214 | + ↓ |
| 215 | +13. Renderer updates authStore |
| 216 | + ↓ |
| 217 | +14. Renderer creates PostHog client with access token |
| 218 | + ↓ |
| 219 | +15. Schedule proactive token refresh |
| 220 | + ↓ |
| 221 | +16. Authenticated! 🎉 |
| 222 | +``` |
| 223 | +
|
| 224 | +## Token Refresh Flow |
| 225 | +
|
| 226 | +``` |
| 227 | +Proactive Refresh (every ~55 mins for 60-min tokens): |
| 228 | +- setTimeout scheduled on login/refresh |
| 229 | +- Calls refresh endpoint 5 mins before expiry |
| 230 | +- Updates both access + refresh tokens |
| 231 | +- Reschedules next refresh |
| 232 | + |
| 233 | +Reactive Refresh (on 401): |
| 234 | +- API request returns 401 |
| 235 | +- Interceptor calls refreshAccessToken() |
| 236 | +- Retries original request with new token |
| 237 | +- If refresh fails: logout user |
| 238 | +``` |
| 239 | +
|
| 240 | +## Key Dependencies |
| 241 | +
|
| 242 | +- `crypto` (Node.js) - PKCE code generation |
| 243 | +- `http` (Node.js) - Callback server |
| 244 | +- `electron.safeStorage` - Token encryption |
| 245 | +- `open` or `electron.shell.openExternal` - Browser opening |
| 246 | +
|
| 247 | +## Environment Variables |
| 248 | +
|
| 249 | +Consider adding to `.env` (optional): |
| 250 | +```bash |
| 251 | +POSTHOG_OAUTH_CLIENT_ID_US=c4Rdw8DIxgtQfA80IiSnGKlNX8QN00cFWF00QQhM |
| 252 | +POSTHOG_OAUTH_CLIENT_ID_EU=bx2C5sZRN03TkdjraCcetvQFPGH6N2Y9vRLkcKEy |
| 253 | +POSTHOG_OAUTH_CLIENT_ID_DEV=DC5uRLVbGI02YQ82grxgnK6Qn12SXWpCqdPb60oZ |
| 254 | +``` |
| 255 | + |
| 256 | +## Testing Checklist |
| 257 | + |
| 258 | +- [x] OAuth flow completes successfully for US region |
| 259 | +- [x] OAuth flow completes successfully for EU region |
| 260 | +- [x] OAuth flow completes successfully for Dev region |
| 261 | +- [x] Proactive token refresh works |
| 262 | +- [x] Reactive token refresh works on 401 |
| 263 | +- [x] Refresh token rotation updates both tokens |
| 264 | +- [x] Logout clears all OAuth state |
| 265 | +- [x] App restart with valid tokens restores session |
| 266 | +- [x] App restart without tokens shows AuthScreen |
| 267 | +- [x] Timeout handling (60s limit) |
| 268 | +- [x] User denies access handling |
| 269 | +- [x] Port 8239 conflict handling |
| 270 | +- [ ] Network error handling |
| 271 | +- [ ] Token expiry edge cases |
0 commit comments