Skip to content

Commit 84cf205

Browse files
authored
Merge pull request #818 from airweave-ai/feat/pipedream-proxy-auth
feat(pipedream): add proxy auth
2 parents 1bafcf5 + 1f9c995 commit 84cf205

File tree

30 files changed

+934
-195
lines changed

30 files changed

+934
-195
lines changed

.cursor/rules/auth-providers.mdc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,35 @@ token_manager = TokenManager(
9090
- **auth_provider_connections**: User's configured providers (encrypted)
9191
- **source_connections**: Links to auth provider via `auth_provider_connection_id`
9292
# Airweave Auth Providers
93+
94+
95+
### Pipedream Proxy Authentication
96+
97+
Pipedream supports two authentication modes depending on the OAuth client type:
98+
99+
#### Direct Mode (Custom OAuth Clients)
100+
- Used when accounts are connected via custom OAuth clients
101+
- Credentials are retrievable and used directly by sources
102+
- Standard authentication flow
103+
104+
#### Proxy Mode (Default OAuth Clients)
105+
- Required when using Pipedream's default OAuth client
106+
- Credentials are not exposed for security
107+
- All API requests route through Pipedream's proxy endpoint
108+
- Pipedream injects authentication credentials server-side
109+
110+
The system automatically detects which mode to use:
111+
1. Attempts to retrieve credentials
112+
2. If unavailable (default OAuth), switches to proxy mode
113+
3. Sources use `PipedreamProxyClient` transparently
114+
115+
**Benefits:**
116+
- Supports both custom and default OAuth clients
117+
- Maintains security for default OAuth credentials
118+
- Transparent to source implementations (same API)
119+
- No code changes needed in individual sources
120+
121+
**Technical Details:**
122+
- Proxy URL: `https://api.pipedream.com/v1/connect/{project_id}/proxy/{encoded_url}`
123+
- Auth headers stripped and injected by Pipedream
124+
- Non-auth headers forwarded with `x-pd-proxy-` prefix

.github/workflows/test-public-api.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ jobs:
5353
TEST_COMPOSIO_API_KEY: ${{ secrets.TEST_COMPOSIO_API_KEY }}
5454
TEST_COMPOSIO_AUTH_CONFIG_ID: ${{ secrets.TEST_COMPOSIO_AUTH_CONFIG_ID }}
5555
TEST_COMPOSIO_ACCOUNT_ID: ${{ secrets.TEST_COMPOSIO_ACCOUNT_ID }}
56+
TEST_PIPEDREAM_CLIENT_ID: ${{ secrets.TEST_PIPEDREAM_CLIENT_ID }}
57+
TEST_PIPEDREAM_CLIENT_SECRET: ${{ secrets.TEST_PIPEDREAM_CLIENT_SECRET }}
58+
TEST_PIPEDREAM_PROJECT_ID: ${{ secrets.TEST_PIPEDREAM_PROJECT_ID }}
59+
TEST_PIPEDREAM_ACCOUNT_ID: ${{ secrets.TEST_PIPEDREAM_ACCOUNT_ID }}
60+
TEST_PIPEDREAM_EXTERNAL_USER_ID: ${{ secrets.TEST_PIPEDREAM_EXTERNAL_USER_ID }}
5661
# Override images to use locally built test images
5762
BACKEND_IMAGE: test-backend:latest
5863
FRONTEND_IMAGE: test-frontend:latest

backend/airweave/platform/auth_providers/_base.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any, Dict, List, Optional
55

66
from airweave.core.logging import logger
7+
from airweave.platform.auth_providers.auth_result import AuthResult
78

89

910
class BaseAuthProvider(ABC):
@@ -65,3 +66,22 @@ async def validate(self) -> bool:
6566
HTTPException: If validation fails with detailed error message
6667
"""
6768
pass
69+
70+
async def get_auth_result(
71+
self, source_short_name: str, source_auth_config_fields: List[str]
72+
) -> AuthResult:
73+
"""Get auth result with explicit mode (direct vs proxy).
74+
75+
Default implementation calls get_creds_for_source and returns direct mode.
76+
Subclasses can override to return proxy mode when needed.
77+
78+
Args:
79+
source_short_name: The short name of the source
80+
source_auth_config_fields: The fields required for the source auth config
81+
82+
Returns:
83+
AuthResult with explicit mode and credentials/config
84+
"""
85+
# Default: try to get credentials directly
86+
credentials = await self.get_creds_for_source(source_short_name, source_auth_config_fields)
87+
return AuthResult.direct(credentials)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Auth result types for explicit auth mode communication."""
2+
3+
from dataclasses import dataclass
4+
from enum import Enum
5+
from typing import Any, Dict, Optional
6+
7+
8+
class AuthProviderMode(Enum):
9+
"""Explicit auth mode enumeration."""
10+
11+
DIRECT = "direct" # Use credentials directly
12+
PROXY = "proxy" # Route through proxy (e.g., Pipedream)
13+
14+
15+
@dataclass
16+
class AuthResult:
17+
"""Result of auth provider credential fetch.
18+
19+
This makes explicit whether to use credentials directly or via proxy.
20+
"""
21+
22+
mode: AuthProviderMode
23+
credentials: Optional[Any] = None # Actual credentials (if DIRECT mode)
24+
proxy_config: Optional[Dict[str, Any]] = None # Config for proxy (if PROXY mode)
25+
26+
@classmethod
27+
def direct(cls, credentials: Any) -> "AuthResult":
28+
"""Create a direct auth result with credentials."""
29+
return cls(mode=AuthProviderMode.DIRECT, credentials=credentials)
30+
31+
@classmethod
32+
def proxy(cls, config: Optional[Dict[str, Any]] = None) -> "AuthResult":
33+
"""Create a proxy auth result."""
34+
return cls(mode=AuthProviderMode.PROXY, proxy_config=config or {})
35+
36+
@property
37+
def requires_proxy(self) -> bool:
38+
"""Check if proxy is required."""
39+
return self.mode == AuthProviderMode.PROXY

backend/airweave/platform/auth_providers/pipedream.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,30 @@
88

99
from airweave.core.credential_sanitizer import safe_log_credentials
1010
from airweave.platform.auth_providers._base import BaseAuthProvider
11+
from airweave.platform.auth_providers.auth_result import AuthResult
1112
from airweave.platform.decorators import auth_provider
1213

1314

15+
class PipedreamDefaultOAuthException(Exception):
16+
"""Raised when trying to access credentials for a Pipedream default OAuth client.
17+
18+
This happens when the connected account uses Pipedream's built-in OAuth client
19+
rather than a custom OAuth client. In this case, credentials cannot be retrieved
20+
directly and the proxy must be used.
21+
"""
22+
23+
def __init__(self, source_short_name: str, message: str = None):
24+
"""Initialize the exception."""
25+
self.source_short_name = source_short_name
26+
if message is None:
27+
message = (
28+
f"Cannot retrieve credentials for {source_short_name}. "
29+
"This account uses Pipedream's default OAuth client. "
30+
"Proxy mode must be used for this connection."
31+
)
32+
super().__init__(message)
33+
34+
1435
@auth_provider(
1536
name="Pipedream",
1637
short_name="pipedream",
@@ -98,10 +119,10 @@ async def create(
98119
instance = cls()
99120
instance.client_id = credentials["client_id"]
100121
instance.client_secret = credentials["client_secret"]
101-
instance.project_id = config.get("project_id")
102-
instance.account_id = config.get("account_id")
122+
instance.project_id = config["project_id"]
123+
instance.account_id = config["account_id"]
124+
instance.external_user_id = config["external_user_id"]
103125
instance.environment = config.get("environment", "production")
104-
instance.external_user_id = config.get("external_user_id")
105126

106127
# Initialize token management
107128
instance._access_token = None
@@ -369,11 +390,7 @@ async def _get_account_with_credentials(
369390
"❌ [Pipedream] No credentials in response. This usually means the account "
370391
"was created with Pipedream's default OAuth client, not a custom one."
371392
)
372-
raise HTTPException(
373-
status_code=422,
374-
detail="Credentials not available. Pipedream only exposes credentials for "
375-
"accounts created with custom OAuth clients, not default Pipedream OAuth.",
376-
)
393+
raise PipedreamDefaultOAuthException(source_short_name)
377394

378395
self.logger.info(
379396
f"✅ [Pipedream] Found account '{account_data.get('name')}' "
@@ -464,3 +481,43 @@ def _extract_and_map_credentials(
464481
)
465482

466483
return found_credentials
484+
485+
async def get_auth_result(
486+
self, source_short_name: str, source_auth_config_fields: List[str]
487+
) -> AuthResult:
488+
"""Get auth result with explicit mode for Pipedream.
489+
490+
Determines whether to use direct credentials or proxy based on OAuth client type.
491+
"""
492+
# Check if source is in blocked list (must use proxy)
493+
if source_short_name in self.BLOCKED_SOURCES:
494+
self.logger.info(f"Source {source_short_name} is in blocked list - using proxy mode")
495+
return AuthResult.proxy(
496+
{
497+
"reason": "blocked_source",
498+
"source": source_short_name,
499+
}
500+
)
501+
502+
# Try to get credentials to determine OAuth client type
503+
try:
504+
credentials = await self.get_creds_for_source(
505+
source_short_name, source_auth_config_fields
506+
)
507+
# Custom OAuth client - can use direct access
508+
self.logger.info(
509+
f"Custom OAuth client detected for {source_short_name} - using direct mode"
510+
)
511+
return AuthResult.direct(credentials)
512+
513+
except PipedreamDefaultOAuthException:
514+
# Default OAuth client - must use proxy
515+
self.logger.info(
516+
f"Default OAuth client detected for {source_short_name} - using proxy mode"
517+
)
518+
return AuthResult.proxy(
519+
{
520+
"reason": "default_oauth",
521+
"source": source_short_name,
522+
}
523+
)

backend/airweave/platform/configs/config.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Configuration classes for platform components."""
22

3-
from typing import Optional
4-
53
from pydantic import Field, validator
64

75
from airweave.platform.configs._base import BaseConfig
@@ -304,13 +302,12 @@ class PipedreamConfig(AuthProviderConfig):
304302
title="Account ID",
305303
description="Pipedream account ID (e.g., apn_gyha5Ky)",
306304
)
305+
external_user_id: str = Field(
306+
title="External User ID",
307+
description="External user ID associated with the account",
308+
)
307309
environment: str = Field(
308310
default="production",
309311
title="Environment",
310312
description="Pipedream environment (production or development)",
311313
)
312-
external_user_id: Optional[str] = Field(
313-
default=None,
314-
title="External User ID",
315-
description="External user ID associated with the account",
316-
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""HTTP client implementations for Airweave platform."""
2+
3+
from .pipedream_proxy import PipedreamProxyClient
4+
5+
__all__ = ["PipedreamProxyClient"]

0 commit comments

Comments
 (0)