Skip to content

Conversation

@mschfh
Copy link
Collaborator

@mschfh mschfh commented Nov 6, 2025

No description provided.

@mschfh mschfh mentioned this pull request Nov 6, 2025
@mschfh mschfh self-assigned this Nov 6, 2025
@github-actions
Copy link

github-actions bot commented Nov 6, 2025

The security team is monitoring all repositories for certain keywords. This PR includes the word(s) "oauth" and so security team members have been added as reviewers to take a look.

No need to request a full security review at this stage, the security team will take a look shortly and either clear the label or request more information/changes.

Notifications have already been sent, but if this is blocking your merge feel free to reach out directly to the security team on Slack so that we can expedite this check.

@mschfh mschfh marked this pull request as ready for review November 13, 2025 20:09
Copilot AI review requested due to automatic review settings November 13, 2025 20:09
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds OAuth proxy functionality to support multiple cryptocurrency exchange providers (Gemini, Bitflyer, Uphold, and Zebpay) with both sandbox and production environments. The proxies handle OAuth authorization redirects and token exchange requests while securely injecting provider credentials server-side.

Key Changes:

  • Added OAuth proxy endpoints for four providers with environment-specific configurations
  • Implemented nested environment variable configuration using OAUTH__ prefix with double-underscore delimiter
  • Added comprehensive test suite using respx for HTTP mocking

Reviewed Changes

Copilot reviewed 17 out of 21 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
pyproject.toml Added respx testing dependency for HTTP mocking
poetry.lock Updated lock file with respx and dependency group changes
env.test Added test environment variables for OAuth provider credentials
env.local Added local development template for OAuth configuration
docker-compose.yml Added Docker Compose setup for application and Redis
conftest.py Added pytest configuration to load test environment variables
app/main.py Registered OAuth router with the FastAPI application
app/config.py Added OAuth configuration with nested environment variable support
app/api/oauth/config.py Defined configuration models for OAuth providers
app/api/oauth/models.py Added Environment enum for sandbox/production
app/api/oauth/routes.py Created main OAuth router aggregating provider routers
app/api/oauth/gemini.py Implemented Gemini OAuth proxy (credentials in JSON body)
app/api/oauth/bitflyer.py Implemented Bitflyer OAuth proxy (credentials in JSON body + Basic Auth)
app/api/oauth/uphold.py Implemented Uphold OAuth proxy (client_id in URL path, Basic Auth)
app/api/oauth/zebpay.py Implemented Zebpay OAuth proxy (nested returnUrl handling, Basic Auth)
app/api/oauth/test_gemini.py Added comprehensive tests for Gemini proxy
app/api/oauth/test_bitflyer.py Added comprehensive tests for Bitflyer proxy
app/api/oauth/test_uphold.py Added comprehensive tests for Uphold proxy
app/api/oauth/test_zebpay.py Added comprehensive tests for Zebpay proxy
app/api/oauth/test_helpers.py Added shared test utilities for URL assertion

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +16 to +18
Sets client_id query param inside the returnUrl query parameter
Example: GET /api/oauth/zebpay/sandbox/auth
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The docstring states "Sets client_id query param inside the returnUrl query parameter" which is accurate but could be clearer about the nested query parameter structure.

Consider updating for better clarity:

"""
Redirect to Zebpay OAuth authorization page.
Injects client_id as a query parameter within the nested returnUrl parameter.

Example: GET /api/oauth/zebpay/sandbox/auth?returnUrl=https://example.com/callback
Result: Redirects to Zebpay with returnUrl containing client_id
"""
Suggested change
Sets client_id query param inside the returnUrl query parameter
Example: GET /api/oauth/zebpay/sandbox/auth
Injects client_id as a query parameter within the nested returnUrl parameter.
Example: GET /api/oauth/zebpay/sandbox/auth?returnUrl=https://example.com/callback
Result: Redirects to Zebpay with returnUrl containing client_id

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +68
return JSONResponse(
content=response.json() if response.content else {},
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response.json() call can raise JSONDecodeError if the response body is not valid JSON. This should be handled to avoid unhandled exceptions.

Consider wrapping this in a try-except:

try:
    content = response.json() if response.content else {}
except ValueError:
    content = {}
Suggested change
return JSONResponse(
content=response.json() if response.content else {},
try:
content = response.json() if response.content else {}
except ValueError:
content = {}
return JSONResponse(
content=content,

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +65
return JSONResponse(
content=response.json() if response.content else {},
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response.json() call can raise JSONDecodeError if the response body is not valid JSON. This should be handled to avoid unhandled exceptions.

Consider wrapping this in a try-except:

try:
    content = response.json() if response.content else {}
except ValueError:
    content = {}
Suggested change
return JSONResponse(
content=response.json() if response.content else {},
try:
content = response.json() if response.content else {}
except ValueError:
content = {}
return JSONResponse(
content=content,

Copilot uses AI. Check for mistakes.
async def auth(environment: Environment, request: Request) -> RedirectResponse:
"""
Redirect to Uphold OAuth authorization page.
Sets client_id query param.
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring states "Sets client_id query param" but the implementation actually puts the client_id in the URL path, not as a query parameter. This is misleading.

Update the docstring to match the implementation:

"""
Redirect to Uphold OAuth authorization page.
Sets client_id in the URL path.

Example: GET /api/oauth/uphold/sandbox/auth
"""
Suggested change
Sets client_id query param.
Sets client_id in the URL path.

Copilot uses AI. Check for mistakes.
@kdenhartog
Copy link
Member

@onyb are you good to maintain this even though most of it's related to rewards work?

@onyb
Copy link
Member

onyb commented Nov 17, 2025

@kdenhartog Yes, we have agreed to maintain this as long as the proxies are related to "web3", "rewards", etc. These endpoints likely won't have the level of traffic that other gate3 endpoints do, so there are no separate scaling needs either.

Copy link
Member

@onyb onyb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some minor review comments

@github-actions
Copy link

[puLL-Merge] - brave/gate3@126

Description

This PR adds OAuth proxy functionality for four cryptocurrency exchange providers (Gemini, Bitflyer, Uphold, and Zebpay). The implementation provides authentication and token exchange endpoints that act as intermediaries between client applications and OAuth providers, injecting client credentials server-side rather than exposing them to clients.

Key changes:

  • Adds /api/oauth/{provider}/{environment}/auth endpoints that redirect to provider OAuth pages with injected client_id
  • Adds /api/oauth/{provider}/{environment}/token endpoints that forward token exchange requests with injected credentials
  • Supports both sandbox and production environments for each provider
  • Configuration via environment variables with nested structure (e.g., OAUTH__GEMINI__SANDBOX__CLIENT_ID)

Possible Issues

  1. Missing Rate Limiting: The OAuth proxy endpoints have no rate limiting, which could be abused to make excessive requests to upstream providers or for credential stuffing attacks.

  2. Timeout Hardcoded: All HTTP requests use a hardcoded 30-second timeout. This should be configurable as different providers may have different latency characteristics.

  3. No Request Validation: The proxy blindly forwards all query parameters and request bodies without validation. Malicious clients could inject unexpected parameters that might cause issues with upstream providers.

  4. Error Detail Leakage: Generic error messages like "Gemini request failed" provide minimal information for legitimate debugging while potentially masking security-relevant errors.

  5. Missing CORS Configuration: The new endpoints don't appear to have explicit CORS configuration, which could cause issues for browser-based OAuth flows.

  6. No Metrics/Monitoring: No observable metrics for OAuth request success/failure rates, latencies, or error types.

Security Hotspots

  1. Credential Exposure in Logs (HIGH): In gemini.py, bitflyer.py, and other provider files, the full request body containing client_secret is logged when using default httpx logging. If verbose logging is enabled, secrets could be written to logs.

    body_dict["client_secret"] = env_config.client_secret
  2. Open Redirect Vulnerability in Zebpay (HIGH): In zebpay.py line 27, the returnUrl parameter is user-controlled and not validated:

    req_return_url = query_params.get("returnUrl")
    if not req_return_url:
        raise HTTPException(status_code=400, detail="Missing returnUrl parameter")
    return_url = URL(req_return_url).include_query_params(client_id=env_config.client_id)

    An attacker could provide a malicious URL like https://evil.com and the proxy would redirect users there with the client_id in the query string.

  3. Unvalidated Redirect URLs (MEDIUM): The auth endpoints in all providers accept arbitrary redirect_uri parameters from clients and forward them to OAuth providers. While providers should validate these, defense-in-depth suggests the proxy should validate against a whitelist.

  4. State Parameter Not Validated (MEDIUM): OAuth state parameters are passed through without validation. The proxy should enforce that state tokens meet minimum entropy requirements to prevent CSRF attacks.

  5. No Input Sanitization (MEDIUM): Query parameters and request bodies are forwarded without sanitization, which could allow header injection or other attacks if providers don't properly validate inputs.

Privacy Hotspots

  1. Access Token Exposure in Responses (HIGH): All token endpoints return access tokens and refresh tokens directly in JSON responses without any encryption or additional protection. These tokens grant access to users' financial accounts.

    return JSONResponse(
        content=response.json() if response.content else {},
        status_code=response.status_code,
    )
  2. Authorization Codes in URLs (MEDIUM): OAuth authorization codes are passed as query parameters which may be logged in proxy logs, application logs, or HTTP access logs.

  3. No Request Logging Sanitization (MEDIUM): If request logging is enabled, authorization codes, tokens, and other sensitive OAuth parameters would be logged in plain text.

  4. Credentials in Environment Variables (LOW): Secrets are stored in environment variables which is acceptable but could be improved with a dedicated secrets management system for production deployments.

Changes

Changes

app/api/oauth/__init__.py

  • Empty init file to make oauth a Python package

app/api/oauth/config.py

  • Defines Pydantic configuration models for OAuth provider settings
  • EnvironmentConfig: Base config with oauth_url, client_id, client_secret
  • ProviderConfigBase: Base class with get_env_config() helper
  • GeminiConfig, BitflyerConfig: Basic OAuth configs
  • UpholdConfig, ZebpayConfig: Extended configs with additional api_url field
  • OAuthConfig: Root config container for all providers

app/api/oauth/models.py

  • Defines Environment enum with SANDBOX and PRODUCTION values
  • Used for path parameters to select environment

app/api/oauth/gemini.py

  • GET /{environment}/auth: Redirects to Gemini OAuth with client_id injected
  • POST /{environment}/token: Forwards token exchange, injects credentials in JSON body

app/api/oauth/bitflyer.py

  • GET /{environment}/auth: Redirects to Bitflyer OAuth with client_id
  • POST /{environment}/token: Forwards token exchange, uses Basic Auth header + credentials in body

app/api/oauth/uphold.py

  • GET /{environment}/auth: Redirects to Uphold OAuth with client_id in URL path (unique pattern)
  • POST /{environment}/token: Forwards form-urlencoded token exchange with Basic Auth, uses api_url instead of oauth_url

app/api/oauth/zebpay.py

  • GET /{environment}/auth: Complex redirect handling - parses returnUrl parameter and injects client_id into it
  • POST /{environment}/token: Forwards form-urlencoded token exchange with Basic Auth, uses api_url

app/api/oauth/routes.py

  • Central router that includes all provider routers under /api/oauth prefix

app/api/oauth/test_*.py files

  • Comprehensive tests for each provider using respx to mock HTTP calls
  • Tests verify auth redirects, successful token exchange, error forwarding
  • Tests validate credential injection and request forwarding

app/api/oauth/test_helpers.py

  • assert_redirect(): Helper function to validate redirect URLs and query parameters in tests

app/config.py

  • Adds oauth: OAuthConfig field to Settings
  • Configures env_nested_delimiter="__" for nested environment variable parsing
  • Adds ENV_FILE environment variable support for test configuration

app/main.py

  • Includes oauth_router in main FastAPI application

conftest.py

  • Root pytest configuration
  • Sets ENV_FILE=env.test before Settings initialization

docker-compose.yml

  • Adds docker-compose configuration with app and redis services
  • Volume mounts for development
  • Port mappings for app (8000) and Prometheus (8090)

env.local and env.test

  • Template environment files with all OAuth provider configurations
  • Test file contains mock URLs and credentials for testing

poetry.lock and pyproject.toml

  • Adds respx = "0.22.0" dev dependency for HTTP mocking in tests
sequenceDiagram
    participant Client
    participant Gate3 Proxy
    participant OAuth Provider
    participant Token Endpoint

    Note over Client,Token Endpoint: Authorization Flow
    Client->>Gate3 Proxy: GET /api/oauth/{provider}/{env}/auth?params
    Gate3 Proxy->>Gate3 Proxy: Inject client_id
    Gate3 Proxy->>Client: 302 Redirect to Provider
    Client->>OAuth Provider: Follow redirect with client_id
    OAuth Provider->>Client: User authorizes, redirects with code
    
    Note over Client,Token Endpoint: Token Exchange Flow
    Client->>Gate3 Proxy: POST /api/oauth/{provider}/{env}/token<br/>{code, grant_type, ...}
    Gate3 Proxy->>Gate3 Proxy: Inject client_id & client_secret
    Gate3 Proxy->>Token Endpoint: Forward request with credentials
    Token Endpoint->>Gate3 Proxy: Return tokens
    Gate3 Proxy->>Client: Forward token response
Loading

Copy link
Member

@onyb onyb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Please remember to set the environment variables on staging and production before merging this PR. We have auto-deploy setup for this project.

@mihaiplesa mihaiplesa requested a review from kdenhartog December 4, 2025 10:52
Copy link
Member

@kdenhartog kdenhartog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a security perspective this seems fine. I checked to see if there's any additional providers that support PKCE with authorization code flow. That's the ideal scenario here and we're using it when we can it seems.

The one nit I did have when reading through this is that we could likely refactor this PR so that there's less code here. Most of these implementations are doing the same thing, but with a different URL and query params passed in by the caller.

@kdenhartog
Copy link
Member

@mschfh asked over DM:

redirect_uri which is just passed through, should we also validate/override it, or leave that to the auth provider?

I think it does make sense to validate this just as a low hanging fruit to make sure people don't abuse this API access in some way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants