PlanOpticon

planopticon / docs / api / auth.md
Source Blame History 377 lines
3551b80… noreply 1 # Auth API Reference
3551b80… noreply 2
3551b80… noreply 3 ::: video_processor.auth
3551b80… noreply 4
3551b80… noreply 5 ---
3551b80… noreply 6
3551b80… noreply 7 ## Overview
3551b80… noreply 8
3551b80… noreply 9 The `video_processor.auth` module provides a unified OAuth and authentication strategy for all PlanOpticon source connectors. It supports multiple authentication methods tried in a consistent order:
3551b80… noreply 10
3551b80… noreply 11 1. **Saved token** -- load from disk, auto-refresh if expired
3551b80… noreply 12 2. **Client Credentials** -- server-to-server OAuth (e.g., Zoom S2S)
3551b80… noreply 13 3. **OAuth 2.0 PKCE** -- interactive Authorization Code flow with PKCE
3551b80… noreply 14 4. **API key fallback** -- environment variable lookup
3551b80… noreply 15
3551b80… noreply 16 Tokens are persisted to `~/.planopticon/` and automatically refreshed on expiry.
3551b80… noreply 17
3551b80… noreply 18 ---
3551b80… noreply 19
3551b80… noreply 20 ## AuthConfig
3551b80… noreply 21
3551b80… noreply 22 ```python
3551b80… noreply 23 from video_processor.auth import AuthConfig
3551b80… noreply 24 ```
3551b80… noreply 25
3551b80… noreply 26 Dataclass configuring authentication for a specific service. Defines OAuth endpoints, client credentials, API key fallback, scopes, and token storage.
3551b80… noreply 27
3551b80… noreply 28 ### Fields
3551b80… noreply 29
3551b80… noreply 30 | Field | Type | Default | Description |
3551b80… noreply 31 |---|---|---|---|
3551b80… noreply 32 | `service` | `str` | *required* | Service identifier (e.g., `"zoom"`, `"notion"`) |
3551b80… noreply 33 | `oauth_authorize_url` | `Optional[str]` | `None` | OAuth authorization endpoint URL |
3551b80… noreply 34 | `oauth_token_url` | `Optional[str]` | `None` | OAuth token exchange endpoint URL |
3551b80… noreply 35 | `client_id` | `Optional[str]` | `None` | OAuth client ID (direct value) |
3551b80… noreply 36 | `client_secret` | `Optional[str]` | `None` | OAuth client secret (direct value) |
3551b80… noreply 37 | `client_id_env` | `Optional[str]` | `None` | Environment variable for client ID |
3551b80… noreply 38 | `client_secret_env` | `Optional[str]` | `None` | Environment variable for client secret |
3551b80… noreply 39 | `api_key_env` | `Optional[str]` | `None` | Environment variable for API key fallback |
3551b80… noreply 40 | `scopes` | `List[str]` | `[]` | OAuth scopes to request |
3551b80… noreply 41 | `redirect_uri` | `str` | `"urn:ietf:wg:oauth:2.0:oob"` | Redirect URI for auth code flow |
3551b80… noreply 42 | `account_id` | `Optional[str]` | `None` | Account ID for client credentials grant (direct value) |
3551b80… noreply 43 | `account_id_env` | `Optional[str]` | `None` | Environment variable for account ID |
3551b80… noreply 44 | `token_path` | `Optional[Path]` | `None` | Custom token storage path |
3551b80… noreply 45
3551b80… noreply 46 ### Resolved Properties
3551b80… noreply 47
3551b80… noreply 48 These properties resolve values by checking the direct field first, then falling back to the environment variable.
3551b80… noreply 49
3551b80… noreply 50 | Property | Return Type | Description |
3551b80… noreply 51 |---|---|---|
3551b80… noreply 52 | `resolved_client_id` | `Optional[str]` | Client ID from `client_id` or `os.environ[client_id_env]` |
3551b80… noreply 53 | `resolved_client_secret` | `Optional[str]` | Client secret from `client_secret` or `os.environ[client_secret_env]` |
3551b80… noreply 54 | `resolved_api_key` | `Optional[str]` | API key from `os.environ[api_key_env]` |
3551b80… noreply 55 | `resolved_account_id` | `Optional[str]` | Account ID from `account_id` or `os.environ[account_id_env]` |
3551b80… noreply 56 | `resolved_token_path` | `Path` | Token file path: `token_path` or `~/.planopticon/{service}_token.json` |
3551b80… noreply 57 | `supports_oauth` | `bool` | `True` if both `oauth_authorize_url` and `oauth_token_url` are set |
3551b80… noreply 58
3551b80… noreply 59 ```python
3551b80… noreply 60 from video_processor.auth import AuthConfig
3551b80… noreply 61
3551b80… noreply 62 config = AuthConfig(
3551b80… noreply 63 service="notion",
3551b80… noreply 64 oauth_authorize_url="https://api.notion.com/v1/oauth/authorize",
3551b80… noreply 65 oauth_token_url="https://api.notion.com/v1/oauth/token",
3551b80… noreply 66 client_id_env="NOTION_CLIENT_ID",
3551b80… noreply 67 client_secret_env="NOTION_CLIENT_SECRET",
3551b80… noreply 68 api_key_env="NOTION_API_KEY",
3551b80… noreply 69 scopes=["read_content"],
3551b80… noreply 70 )
3551b80… noreply 71
3551b80… noreply 72 # Check resolved values
3551b80… noreply 73 print(config.resolved_client_id) # From NOTION_CLIENT_ID env var
3551b80… noreply 74 print(config.supports_oauth) # True
3551b80… noreply 75 print(config.resolved_token_path) # ~/.planopticon/notion_token.json
3551b80… noreply 76 ```
3551b80… noreply 77
3551b80… noreply 78 ---
3551b80… noreply 79
3551b80… noreply 80 ## AuthResult
3551b80… noreply 81
3551b80… noreply 82 ```python
3551b80… noreply 83 from video_processor.auth import AuthResult
3551b80… noreply 84 ```
3551b80… noreply 85
3551b80… noreply 86 Dataclass representing the result of an authentication attempt.
3551b80… noreply 87
3551b80… noreply 88 | Field | Type | Default | Description |
3551b80… noreply 89 |---|---|---|---|
3551b80… noreply 90 | `success` | `bool` | *required* | Whether authentication succeeded |
3551b80… noreply 91 | `access_token` | `Optional[str]` | `None` | The access token (if successful) |
3551b80… noreply 92 | `method` | `Optional[str]` | `None` | Auth method used: `"saved_token"`, `"oauth_pkce"`, `"client_credentials"`, `"api_key"` |
3551b80… noreply 93 | `expires_at` | `Optional[float]` | `None` | Token expiration as Unix timestamp |
3551b80… noreply 94 | `refresh_token` | `Optional[str]` | `None` | OAuth refresh token (if available) |
3551b80… noreply 95 | `error` | `Optional[str]` | `None` | Error message (if failed) |
3551b80… noreply 96
3551b80… noreply 97 ```python
3551b80… noreply 98 result = manager.authenticate()
3551b80… noreply 99 if result.success:
3551b80… noreply 100 print(f"Authenticated via {result.method}")
3551b80… noreply 101 print(f"Token: {result.access_token[:20]}...")
3551b80… noreply 102 if result.expires_at:
3551b80… noreply 103 import time
3551b80… noreply 104 remaining = result.expires_at - time.time()
3551b80… noreply 105 print(f"Expires in {remaining/60:.0f} minutes")
3551b80… noreply 106 else:
3551b80… noreply 107 print(f"Auth failed: {result.error}")
3551b80… noreply 108 ```
3551b80… noreply 109
3551b80… noreply 110 ---
3551b80… noreply 111
3551b80… noreply 112 ## OAuthManager
3551b80… noreply 113
3551b80… noreply 114 ```python
3551b80… noreply 115 from video_processor.auth import OAuthManager
3551b80… noreply 116 ```
3551b80… noreply 117
3551b80… noreply 118 Manages the full authentication lifecycle for a service. Tries auth methods in priority order and handles token persistence, refresh, and PKCE flow.
3551b80… noreply 119
3551b80… noreply 120 ### Constructor
3551b80… noreply 121
3551b80… noreply 122 ```python
3551b80… noreply 123 def __init__(self, config: AuthConfig)
3551b80… noreply 124 ```
3551b80… noreply 125
3551b80… noreply 126 | Parameter | Type | Description |
3551b80… noreply 127 |---|---|---|
3551b80… noreply 128 | `config` | `AuthConfig` | Authentication configuration for the target service |
3551b80… noreply 129
3551b80… noreply 130 ### authenticate()
3551b80… noreply 131
3551b80… noreply 132 ```python
3551b80… noreply 133 def authenticate(self) -> AuthResult
3551b80… noreply 134 ```
3551b80… noreply 135
3551b80… noreply 136 Run the full auth chain and return the result. Methods are tried in order:
3551b80… noreply 137
3551b80… noreply 138 1. **Saved token** -- checks `~/.planopticon/{service}_token.json`, refreshes if expired
3551b80… noreply 139 2. **Client Credentials** -- if `account_id` is set and OAuth is configured, uses the client credentials grant (server-to-server)
3551b80… noreply 140 3. **OAuth PKCE** -- if OAuth is configured and client ID is available, opens a browser for interactive authorization with PKCE
3551b80… noreply 141 4. **API key** -- falls back to the environment variable specified in `api_key_env`
3551b80… noreply 142
3551b80… noreply 143 **Returns:** `AuthResult` -- success/failure with token and method details.
3551b80… noreply 144
3551b80… noreply 145 If all methods fail, returns an `AuthResult` with `success=False` and a helpful error message listing which environment variables to set.
3551b80… noreply 146
3551b80… noreply 147 ### get_token()
3551b80… noreply 148
3551b80… noreply 149 ```python
3551b80… noreply 150 def get_token(self) -> Optional[str]
3551b80… noreply 151 ```
3551b80… noreply 152
3551b80… noreply 153 Convenience method: run `authenticate()` and return just the access token string.
3551b80… noreply 154
3551b80… noreply 155 **Returns:** `Optional[str]` -- the access token, or `None` if authentication failed.
3551b80… noreply 156
3551b80… noreply 157 ### clear_token()
3551b80… noreply 158
3551b80… noreply 159 ```python
3551b80… noreply 160 def clear_token(self) -> None
3551b80… noreply 161 ```
3551b80… noreply 162
3551b80… noreply 163 Remove the saved token file for this service (effectively a logout). The next `authenticate()` call will require re-authentication.
3551b80… noreply 164
3551b80… noreply 165 ---
3551b80… noreply 166
3551b80… noreply 167 ## Authentication Flows
3551b80… noreply 168
3551b80… noreply 169 ### Saved Token (auto-refresh)
3551b80… noreply 170
3551b80… noreply 171 Tokens are saved to `~/.planopticon/{service}_token.json` as JSON. On each `authenticate()` call, the saved token is loaded and checked:
3551b80… noreply 172
3551b80… noreply 173 - If the token has not expired (`time.time() < expires_at`), it is returned immediately
3551b80… noreply 174 - If expired but a refresh token is available, the manager attempts to refresh using the OAuth token endpoint
3551b80… noreply 175 - The refreshed token is saved back to disk
3551b80… noreply 176
3551b80… noreply 177 ### Client Credentials Grant
3551b80… noreply 178
3551b80… noreply 179 Used for server-to-server authentication (e.g., Zoom Server-to-Server OAuth). Requires `account_id`, `client_id`, and `client_secret`. Sends a POST to the token endpoint with `grant_type=account_credentials`.
3551b80… noreply 180
3551b80… noreply 181 ### OAuth 2.0 Authorization Code with PKCE
3551b80… noreply 182
3551b80… noreply 183 Interactive flow for user authentication:
3551b80… noreply 184
3551b80… noreply 185 1. Generates a PKCE code verifier and S256 challenge
3551b80… noreply 186 2. Constructs the authorization URL with client ID, redirect URI, scopes, and PKCE challenge
3551b80… noreply 187 3. Opens the URL in the user's browser
3551b80… noreply 188 4. Prompts the user to paste the authorization code
3551b80… noreply 189 5. Exchanges the code for tokens at the token endpoint
3551b80… noreply 190 6. Saves the tokens to disk
3551b80… noreply 191
3551b80… noreply 192 ### API Key Fallback
3551b80… noreply 193
3551b80… noreply 194 If no OAuth flow succeeds, falls back to checking the environment variable specified in `api_key_env`. Returns the value directly as the access token.
3551b80… noreply 195
3551b80… noreply 196 ---
3551b80… noreply 197
3551b80… noreply 198 ## KNOWN_CONFIGS
3551b80… noreply 199
3551b80… noreply 200 ```python
3551b80… noreply 201 from video_processor.auth import KNOWN_CONFIGS
3551b80… noreply 202 ```
3551b80… noreply 203
3551b80… noreply 204 Pre-built `AuthConfig` instances for supported services. These cover the most common cloud integrations and can be used directly or as templates for custom configurations.
3551b80… noreply 205
3551b80… noreply 206 | Service Key | Service | OAuth Endpoints | Client ID Env | API Key Env |
3551b80… noreply 207 |---|---|---|---|---|
3551b80… noreply 208 | `"zoom"` | Zoom | `zoom.us/oauth/...` | `ZOOM_CLIENT_ID` | -- |
3551b80… noreply 209 | `"notion"` | Notion | `api.notion.com/v1/oauth/...` | `NOTION_CLIENT_ID` | `NOTION_API_KEY` |
3551b80… noreply 210 | `"dropbox"` | Dropbox | `dropbox.com/oauth2/...` | `DROPBOX_APP_KEY` | `DROPBOX_ACCESS_TOKEN` |
3551b80… noreply 211 | `"github"` | GitHub | `github.com/login/oauth/...` | `GITHUB_CLIENT_ID` | `GITHUB_TOKEN` |
3551b80… noreply 212 | `"google"` | Google | `accounts.google.com/o/oauth2/...` | `GOOGLE_CLIENT_ID` | `GOOGLE_API_KEY` |
3551b80… noreply 213 | `"microsoft"` | Microsoft | `login.microsoftonline.com/.../oauth2/...` | `MICROSOFT_CLIENT_ID` | -- |
3551b80… noreply 214
3551b80… noreply 215 ### Zoom
3551b80… noreply 216
3551b80… noreply 217 Supports both Server-to-Server (via `ZOOM_ACCOUNT_ID`) and OAuth PKCE flows.
3551b80… noreply 218
3551b80… noreply 219 ```bash
3551b80… noreply 220 # Server-to-Server
3551b80… noreply 221 export ZOOM_CLIENT_ID="..."
3551b80… noreply 222 export ZOOM_CLIENT_SECRET="..."
3551b80… noreply 223 export ZOOM_ACCOUNT_ID="..."
3551b80… noreply 224
3551b80… noreply 225 # Or interactive OAuth (omit ZOOM_ACCOUNT_ID)
3551b80… noreply 226 export ZOOM_CLIENT_ID="..."
3551b80… noreply 227 export ZOOM_CLIENT_SECRET="..."
3551b80… noreply 228 ```
3551b80… noreply 229
3551b80… noreply 230 ### Google (Drive, Meet, Workspace)
3551b80… noreply 231
3551b80… noreply 232 Supports OAuth PKCE and API key fallback. Scopes include Drive and Docs read-only access.
3551b80… noreply 233
3551b80… noreply 234 ```bash
3551b80… noreply 235 export GOOGLE_CLIENT_ID="..."
3551b80… noreply 236 export GOOGLE_CLIENT_SECRET="..."
3551b80… noreply 237 # Or for API-key-only access:
3551b80… noreply 238 export GOOGLE_API_KEY="..."
3551b80… noreply 239 ```
3551b80… noreply 240
3551b80… noreply 241 ### GitHub
3551b80… noreply 242
3551b80… noreply 243 Supports OAuth PKCE and personal access token. Requests `repo` and `read:org` scopes.
3551b80… noreply 244
3551b80… noreply 245 ```bash
3551b80… noreply 246 # OAuth
3551b80… noreply 247 export GITHUB_CLIENT_ID="..."
3551b80… noreply 248 export GITHUB_CLIENT_SECRET="..."
3551b80… noreply 249 # Or personal access token
3551b80… noreply 250 export GITHUB_TOKEN="ghp_..."
3551b80… noreply 251 ```
3551b80… noreply 252
3551b80… noreply 253 ---
3551b80… noreply 254
3551b80… noreply 255 ## Helper Functions
3551b80… noreply 256
3551b80… noreply 257 ### get_auth_config()
3551b80… noreply 258
3551b80… noreply 259 ```python
3551b80… noreply 260 def get_auth_config(service: str) -> Optional[AuthConfig]
3551b80… noreply 261 ```
3551b80… noreply 262
3551b80… noreply 263 Get a pre-built `AuthConfig` for a known service.
3551b80… noreply 264
3551b80… noreply 265 **Parameters:**
3551b80… noreply 266
3551b80… noreply 267 | Parameter | Type | Description |
3551b80… noreply 268 |---|---|---|
3551b80… noreply 269 | `service` | `str` | Service name (e.g., `"zoom"`, `"notion"`, `"github"`) |
3551b80… noreply 270
3551b80… noreply 271 **Returns:** `Optional[AuthConfig]` -- the config, or `None` if the service is not in `KNOWN_CONFIGS`.
3551b80… noreply 272
3551b80… noreply 273 ### get_auth_manager()
3551b80… noreply 274
3551b80… noreply 275 ```python
3551b80… noreply 276 def get_auth_manager(service: str) -> Optional[OAuthManager]
3551b80… noreply 277 ```
3551b80… noreply 278
3551b80… noreply 279 Get an `OAuthManager` for a known service. Convenience wrapper that looks up the config and creates the manager in one call.
3551b80… noreply 280
3551b80… noreply 281 **Returns:** `Optional[OAuthManager]` -- the manager, or `None` if the service is not known.
3551b80… noreply 282
3551b80… noreply 283 ---
3551b80… noreply 284
3551b80… noreply 285 ## Usage Examples
3551b80… noreply 286
3551b80… noreply 287 ### Quick authentication for a known service
3551b80… noreply 288
3551b80… noreply 289 ```python
3551b80… noreply 290 from video_processor.auth import get_auth_manager
3551b80… noreply 291
3551b80… noreply 292 manager = get_auth_manager("zoom")
3551b80… noreply 293 if manager:
3551b80… noreply 294 result = manager.authenticate()
3551b80… noreply 295 if result.success:
3551b80… noreply 296 print(f"Authenticated via {result.method}")
3551b80… noreply 297 # Use result.access_token for API calls
3551b80… noreply 298 else:
3551b80… noreply 299 print(f"Failed: {result.error}")
3551b80… noreply 300 ```
3551b80… noreply 301
3551b80… noreply 302 ### Custom service configuration
3551b80… noreply 303
3551b80… noreply 304 ```python
3551b80… noreply 305 from video_processor.auth import AuthConfig, OAuthManager
3551b80… noreply 306
3551b80… noreply 307 config = AuthConfig(
3551b80… noreply 308 service="my_service",
3551b80… noreply 309 oauth_authorize_url="https://my-service.com/oauth/authorize",
3551b80… noreply 310 oauth_token_url="https://my-service.com/oauth/token",
3551b80… noreply 311 client_id_env="MY_SERVICE_CLIENT_ID",
3551b80… noreply 312 client_secret_env="MY_SERVICE_CLIENT_SECRET",
3551b80… noreply 313 api_key_env="MY_SERVICE_API_KEY",
3551b80… noreply 314 scopes=["read", "write"],
3551b80… noreply 315 )
3551b80… noreply 316
3551b80… noreply 317 manager = OAuthManager(config)
3551b80… noreply 318 token = manager.get_token() # Returns str or None
3551b80… noreply 319 ```
3551b80… noreply 320
3551b80… noreply 321 ### Using auth in a custom source connector
3551b80… noreply 322
3551b80… noreply 323 ```python
3551b80… noreply 324 from pathlib import Path
3551b80… noreply 325 from typing import List, Optional
3551b80… noreply 326
3551b80… noreply 327 from video_processor.auth import OAuthManager, AuthConfig
3551b80… noreply 328 from video_processor.sources.base import BaseSource, SourceFile
3551b80… noreply 329
3551b80… noreply 330 class CustomSource(BaseSource):
3551b80… noreply 331 def __init__(self):
3551b80… noreply 332 self._config = AuthConfig(
3551b80… noreply 333 service="custom",
3551b80… noreply 334 api_key_env="CUSTOM_API_KEY",
3551b80… noreply 335 )
3551b80… noreply 336 self._manager = OAuthManager(self._config)
3551b80… noreply 337 self._token: Optional[str] = None
3551b80… noreply 338
3551b80… noreply 339 def authenticate(self) -> bool:
3551b80… noreply 340 self._token = self._manager.get_token()
3551b80… noreply 341 return self._token is not None
3551b80… noreply 342
3551b80… noreply 343 def list_videos(self, **kwargs) -> List[SourceFile]:
3551b80… noreply 344 # Use self._token to query the API
3551b80… noreply 345 ...
3551b80… noreply 346
3551b80… noreply 347 def download(self, file: SourceFile, destination: Path) -> Path:
3551b80… noreply 348 # Use self._token for authenticated downloads
3551b80… noreply 349 ...
3551b80… noreply 350 ```
3551b80… noreply 351
3551b80… noreply 352 ### Logout / clear saved token
3551b80… noreply 353
3551b80… noreply 354 ```python
3551b80… noreply 355 from video_processor.auth import get_auth_manager
3551b80… noreply 356
3551b80… noreply 357 manager = get_auth_manager("zoom")
3551b80… noreply 358 if manager:
3551b80… noreply 359 manager.clear_token()
3551b80… noreply 360 print("Zoom token cleared")
3551b80… noreply 361 ```
3551b80… noreply 362
3551b80… noreply 363 ### Token storage location
3551b80… noreply 364
3551b80… noreply 365 All tokens are stored under `~/.planopticon/`:
3551b80… noreply 366
3551b80… noreply 367 ```
3551b80… noreply 368 ~/.planopticon/
3551b80… noreply 369 zoom_token.json
3551b80… noreply 370 notion_token.json
3551b80… noreply 371 github_token.json
3551b80… noreply 372 google_token.json
3551b80… noreply 373 microsoft_token.json
3551b80… noreply 374 dropbox_token.json
3551b80… noreply 375 ```
3551b80… noreply 376
3551b80… noreply 377 Each file contains a JSON object with `access_token`, `refresh_token` (if applicable), `expires_at`, and client credentials for refresh.

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button