Skip to content

Implement OAuth2 client credentials flow with backward compatibility - #220

Merged
sgandhi1311 merged 20 commits into
masterfrom
byoc-oauth-implmentation
Sep 11, 2026
Merged

sgandhi1311 merged 20 commits into
masterfrom
byoc-oauth-implmentation

Conversation

@prijendev

@prijendev prijendev commented Jun 2, 2026 •

Copy link
Copy Markdown
Contributor

Description of change

Replaces the Salesforce authentication mechanism to support OAuth2 client credentials flow (BYOC — Bring Your Own Credentials), while maintaining full backward compatibility with existing connections created via the
authorization code (refresh token) flow.

What changed

Auth flow — dual support

  • The tap now detects the auth flow at runtime based on config properties:
    • Auth flow - client credentials with legacy fallback
    • New BYOC connections use OAuth2 client credentials with the customer-provided instance_url, client_id, and client_secret.
    • When instance_url is present, the tap attempts the client credentials flow first.
    • Some existing Stitch connections already contain instance_url together with a legacy refresh_token. For these mixed configurations, the tap attempts client credentials first.
    • If Salesforce rejects client credentials and a refresh_token is available, the tap logs a warning and falls back to the existing OAuth2 refresh-token flow. This preserves connectivity for legacy connections whose Salesforce app has not enabled client credentials.
    • Connections with only a refresh_token continue to use the existing refresh-token OAuth flow unchanged.
    • If neither refresh_token nor instance_url is provided, the tap fails with a clear configuration error.
  • Validation raises a clear error if neither is provided
  • instance_url is no longer a hard required key — it is optional for the
    refresh_token path (derived from the token response) and required only for
    client_credentials

Security — SSRF protection on instance_url

  • instance_url is customer-supplied in the client_credentials flow, so it is
    validated before any network request:
    • Rejects http:// (Salesforce requires HTTPS)
    • Auto-prepends https:// if the scheme is missing (with a warning log)
    • Rejects non-Salesforce domains (must contain salesforce.) to prevent a malicious value like http://169.254.169.254 from causing the tap to POST client_id/client_secret to an internal endpoint
  • Addresses the open SSRF concern raised in code review

Token refresh

  • Periodic re-authentication (every 15 min) via threading.Timer is preserved for both flows — access tokens expire; long-running syncs must re-authenticate
  • Refresh token rotation is handled in the refresh_token path: if Salesforce returns a new refresh token, it is written back to the config file

Config shapes

New (client credentials):

{
  "client_id": "...",
  "client_secret": "...",
  "instance_url": "https://<my-domain>.my.salesforce.com",
  "start_date": "2020-01-01T00:00:00Z",
  "api_type": "REST"
}

Legacy (refresh token — unchanged):

{
  "client_id": "...",
  "client_secret": "...",
  "refresh_token": "...",
  "is_sandbox": "false",
  "start_date": "2020-01-01T00:00:00Z",
  "api_type": "REST"
}

QA steps

  • All 16 unit tests pass (python -m pytest tests/unittests/ -v)
  • Manual sync verified with client_credentials config against a sandbox org
  • Manual sync verified with refresh_token config (existing flow unchanged)
  • 10 simultaneous client_credentials token requests all succeeded (no concurrent limit issues)
  • Verified client_credentials authentication using client_id, client_secret, and instance_url.
  • Verified refresh-token-only configurations continue to authenticate using the existing legacy flow.
  • Verified mixed configurations attempt client credentials first and fall back to refresh-token OAuth when Salesforce rejects client credentials.
  • Verified the fallback emits a warning identifying the switch to the legacy refresh-token flow.
  • Unit tests: 28 passed.

Risks

  • The salesforce. domain check could reject legitimate non-standard Salesforce endpoints (e.g. custom vanity domains) — can be relaxed if needed
  • For configurations containing both instance_url and refresh_token, a successful client-credentials login can use the Salesforce client-credentials integration user rather than the user represented by the legacy refresh token. This may change the permissions and data visible to the connection. This precedence is intentional to transition eligible connections to BYOC; a refresh-token fallback remains available when client credentials is not supported by the Salesforce app.

Rollback steps

  • Revert this branch

Related tickets

SAC-32028, SAC-31202, SAC-31200

AI generated code

This PR has been written with the help of GitHub Copilot.

Copilot AI left a comment

Copy link
Copy Markdown

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 migrates the tap’s Salesforce authentication from refresh-token (auth code) OAuth to OAuth2 client_credentials (“BYOC OAuth”), updating configuration, docs, and tests to align with the new flow.

Changes:

  • Replace refresh-token login (and sandbox redirect support) with client_credentials login using a customer-provided instance_url.
  • Update tap-tester/integration and unit tests to remove refresh_token / is_sandbox and validate the new login behavior.
  • Bump version to 3.0.0 and update README + CHANGELOG for the breaking auth change.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/unittests/test_refresh_token_rotation.py Removes refresh-token rotation tests (no longer applicable to client_credentials).
tests/unittests/test_multiline_critical_error_message.py Updates test config to use instance_url and remove refresh-token/sandbox fields.
tests/unittests/test_client_credentials_login.py Adds new unit tests for client_credentials login (success, payload, URL handling, retry behavior).
tests/test_salesforce_sync_canary.py Updates test properties to use instance_url only (drops is_sandbox).
tests/test_salesforce_lookback_window.py Updates test properties to use instance_url only (drops is_sandbox).
tests/sfbase.py Updates tap-tester base expectations/type and credentials env vars for BYOC.
tests/base.py Updates tap-tester base expectations/type and credentials env vars for BYOC.
tap_salesforce/salesforce/init.py Implements client_credentials token request; removes refresh-token rotation logic and stops logging POST bodies.
tap_salesforce/init.py Removes refresh_token config usage and passes instance_url into Salesforce.
setup.py Bumps package version to 3.0.0.
README.md Documents new config format (instance_url) and new auth setup guidance.
CHANGELOG.md Adds 3.0.0 entry documenting the auth migration.
Comments suppressed due to low confidence (1)

tap_salesforce/init.py:31

  • The default CONFIG dict doesn't include instance_url, even though later code expects it to be present (CONFIG['instance_url']). Adding the key here improves clarity and avoids surprising KeyErrors if code paths access CONFIG before args.config is merged.
CONFIG = {
    'client_id': None,
    'client_secret': None,
    'start_date': None
}

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

Comment thread tap_salesforce/__init__.py
Comment thread tap_salesforce/salesforce/__init__.py Outdated
Comment thread tests/unittests/test_client_credentials_login.py
Comment thread README.md
Comment thread README.md Outdated
Comment thread tap_salesforce/salesforce/__init__.py Outdated
prijendev and others added 4 commits June 2, 2026 17:38
Make the tap backward compatible by detecting the auth flow at runtime
based on config properties:
- If 'refresh_token' is present → OAuth authorization code flow
  (existing connections created via Salesforce Connected App/ECA)
- If 'instance_url' is present → OAuth client credentials flow
  (new BYOC connections using ECA client_credentials grant)

Changes:
- Remove 'instance_url' from REQUIRED_CONFIG_KEYS; validate that at
  least one of 'refresh_token' or 'instance_url' is provided
- Add refresh_token, is_sandbox params to Salesforce.__init__
- Branch login() on self.refresh_token: refresh_token flow uses
  login.salesforce.com/test.salesforce.com and derives instance_url
  from the token response; client_credentials flow posts directly to
  {instance_url}/services/oauth2/token
- Restore refresh token rotation handling in the refresh_token path

All 16 existing unit tests pass.
Add _normalize_instance_url() to enforce security constraints on the
customer-supplied instance_url before any network request is made:
- Reject http:// (Salesforce requires HTTPS)
- Auto-prepend https:// if scheme is missing (with a warning)
- Reject non-Salesforce domains to prevent SSRF — a malicious value
  like http://169.254.169.254 would otherwise cause the tap to POST
  client credentials to an arbitrary internal endpoint
- Strip trailing slash once at construction rather than at each usage

Also normalize instance_url derived from the refresh_token response
for consistency.

Addresses the open SSRF concern raised by RushiT0122 in PR #220.

Reverts tests/base.py connection type to "platform.salesforce".
@sgandhi1311 sgandhi1311 changed the title Implement BYOC Oauth Implement OAuth2 client credentials flow with backward compatibility Aug 31, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Comment thread tap_salesforce/salesforce/__init__.py
Comment thread README.md
Comment thread CHANGELOG.md Outdated
- Fix SSRF bypass: parse hostname with urlparse instead of substring
  match to prevent tricks like https://salesforce.com@evil.example
- README: document required ECA OAuth scope (api), client credentials
  flow setup, Run As user requirement, and legacy refresh_token config
- CHANGELOG: clarify entry — both flows are supported, not replaced
Add _use_client_credentials flag set at __init__ time based on whether
instance_url was supplied in config. This locks the flow for the lifetime
of the Salesforce object, so timer-triggered re-auths always use the same
flow regardless of instance_url being populated from the first response.
Prefer the client credentials flow for connections with instance_url.

When Salesforce rejects client credentials and an existing refresh token is
available, retry authentication through the legacy OAuth refresh-token flow and
log the fallback.

This preserves existing Stitch connections, which may already store
instance_url in production, while QTC begins providing instance_url for new
client-credentials connections.
@sgandhi1311
sgandhi1311 requested review from vishalp-dev and a balanced review from Copilot September 11, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Mixed configurations use the wrong authentication precedence, and the SSRF validation lacks regression coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

tap_salesforce/salesforce/init.py:275

  • The new hostname allowlist is the security boundary preventing credentials from being posted to an attacker-controlled host, but none of the client-credentials tests exercise it. Add tests covering plain HTTP, a non-Salesforce hostname, the salesforce.com@evil.example user-info trick, a missing scheme, and the accepted Salesforce domains so future changes cannot silently reopen the SSRF path.
        # Parse the hostname to prevent SSRF via URL tricks (e.g. https://salesforce.com@evil.example)
        hostname = urlparse(instance_url).hostname or ''
        if hostname != 'salesforce.com' and not hostname.endswith('.salesforce.com') and \
                hostname != 'salesforce.mil' and not hostname.endswith('.salesforce.mil'):
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread tap_salesforce/salesforce/__init__.py

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Transient HTTP failures can permanently switch authentication modes, and the documented precedence remains inconsistent with the implementation.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +408 to +410
if self._use_client_credentials and self.refresh_token and isinstance(e, requests.exceptions.HTTPError):
LOGGER.warning("Client credentials login failed; retrying with the legacy refresh_token flow.")
self._use_client_credentials = False
Comment on lines +272 to +275
# Parse the hostname to prevent SSRF via URL tricks (e.g. https://salesforce.com@evil.example)
hostname = urlparse(instance_url).hostname or ''
if hostname != 'salesforce.com' and not hostname.endswith('.salesforce.com') and \
hostname != 'salesforce.mil' and not hostname.endswith('.salesforce.mil'):
Comment on lines +78 to +80
self.assertIn('retrying with the legacy refresh_token flow', logs.output[0])
self.assertEqual(mock_request.call_args_list[0].kwargs['body']['grant_type'], 'client_credentials')
self.assertEqual(mock_request.call_args_list[1].kwargs['body']['grant_type'], 'refresh_token')
@sgandhi1311
sgandhi1311 merged commit 43d9d23 into master Sep 11, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants