Skip to content

fix(oauth): exclude client credentials from body when Authorization header is present and _emit_control_message#877

Merged
Aldo Gonzalez (aldogonzalez8) merged 6 commits intomainfrom
devin/1768348770-oauth-basic-auth-header
Jan 15, 2026
Merged

fix(oauth): exclude client credentials from body when Authorization header is present and _emit_control_message#877
Aldo Gonzalez (aldogonzalez8) merged 6 commits intomainfrom
devin/1768348770-oauth-basic-auth-header

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot commented Jan 14, 2026

Summary

Some OAuth providers (like Gong) require client credentials to be sent ONLY via Basic Authentication header, not in the request body. Previously, the CDK always included client_id and client_secret in the token refresh request body, causing 400 errors when an Authorization header was also configured.

This change modifies build_refresh_request_body() to automatically detect when an Authorization header is present in refresh_request_headers and excludes client credentials from the body in that case.

Key changes:

  • Added conditional logic to check for Authorization header presence
  • Client credentials are excluded from body only when Authorization header is configured
  • Preserved original key ordering in payload dictionary for backward compatibility
  • Added unit tests for both scenarios (with and without Authorization header)

Updates since last revision

  • Removed the use_client_credentials_in_refresh option that was added during initial implementation. After confirming Gong's actual requirements (credentials via Basic auth header), this option was unnecessary complexity.
  • The implementation is now simpler: credentials are automatically excluded from the body when an Authorization header is present in refresh_request_headers.
  • Critical fix for single-use refresh tokens: Fixed _emit_control_message() in SingleUseRefreshTokenOauth2Authenticator to always print control messages to stdout. Previously, when using InMemoryMessageRepository, control messages were only queued but never output to stdout, causing the platform to never receive config updates. This meant single-use refresh tokens were not being persisted after refresh, causing subsequent operations to fail with "Refresh token is invalid or expired" errors.
  • Reverted unrelated changes: Removed accidental changes to declarative_component_schema.py (copyright header removal, use_cache description update) that were introduced by running build commands.

Review & Testing Checklist for Human

  • Verify control message fix: The _emit_control_message() change now always emits to stdout AND to the message repository. Verify this doesn't cause duplicate processing or other side effects.
  • Verify backward compatibility: Existing connectors without refresh_request_headers or without an Authorization header should continue to work unchanged (credentials in body)
  • Review detection logic: The check "Authorization" in headers assumes any Authorization header means credentials are in the header. Consider if there are edge cases where this assumption is wrong (e.g., Bearer tokens)
  • Test with actual OAuth provider: This fix was created to support Gong OAuth. Recommend testing with a real Gong OAuth flow after CDK pre-release is created

Recommended test plan:

  1. Create a CDK pre-release from this branch
  2. Update source-gong connector to use the pre-release CDK
  3. Test OAuth authentication flow with Gong:
    • Verify CHECK operation refreshes token and control message is emitted to stdout
    • Verify subsequent SYNC operation uses the persisted new refresh token
    • Verify connectorConfigurationUpdated: true in the response

Notes

This PR is part of implementing OAuth support for source-gong connector (airbyte#71356). The Gong API was returning 400 errors because it received credentials in both the Basic auth header AND the request body. Additionally, Gong uses single-use refresh tokens, which exposed a bug where control messages weren't being emitted to stdout.

Requested by: @aldogonzalez-airbyte (aldo.gonzalez@airbyte.io)

Link to Devin run: https://app.devin.ai/sessions/bac4e6c3e7684fd1802811a5c8c186a1

Important

Auto-merge enabled.

This PR is set to merge automatically when all requirements are met.

…eader is present

Some OAuth providers (like Gong) require client credentials to be sent ONLY
via Basic Authentication header, not in the request body. Previously, the CDK
always included client_id and client_secret in the body, causing 400 errors
when the Authorization header was also present.

This change automatically detects when an Authorization header is configured
in refresh_request_headers and excludes client credentials from the body.

Added unit tests to verify:
- Credentials excluded from body when Authorization header present
- Credentials included in body when no Authorization header (default behavior)

Co-Authored-By: aldo.gonzalez@airbyte.io <aldo.gonzalez@airbyte.io>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Original prompt from aldo.gonzalez@airbyte.io
Received message in Slack channel #ask-devin-ai:

@Devin this issue seems acccurate to me to omplement Oauth for airbyte connector <https://github.com/airbytehq/airbyte-internal-issues/issues/15647> does it look accurate for you? please double check and challenge if needed
Thread URL: https://airbytehq-team.slack.com/archives/C08BHPUMEPJ/p1768338850454729

#15647 Update Gong RC to support Oauth

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions github-actions bot added the bug Something isn't working label Jan 14, 2026
@github-actions
Copy link
Copy Markdown

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

Testing This CDK Version

You can test this version of the CDK using the following:

# Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@devin/1768348770-oauth-basic-auth-header#egg=airbyte-python-cdk[dev]' --help

# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch devin/1768348770-oauth-basic-auth-header

Helpful Resources

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /autofix - Fixes most formatting and linting issues
  • /poetry-lock - Updates poetry.lock file
  • /test - Runs connector tests with the updated CDK
  • /prerelease - Triggers a prerelease publish with default arguments
  • /poe build - Regenerate git-committed build artifacts, such as the pydantic models which are generated from the manifest JSON schema in YAML.
  • /poe <command> - Runs any poe command in the CDK environment

📝 Edit this welcome message.

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

/prerelease

@aldogonzalez8
Copy link
Copy Markdown
Contributor

Aldo Gonzalez (aldogonzalez8) commented Jan 14, 2026

/prerelease

Prerelease Job Info

This job triggers the publish workflow with default arguments to create a prerelease.

Prerelease job started... Check job output.

✅ Prerelease workflow triggered successfully.

View the publish workflow run: https://github.com/airbytehq/airbyte-python-cdk/actions/runs/20977372780

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 14, 2026

PyTest Results (Fast)

3 826 tests  +2   3 814 ✅ +2   6m 22s ⏱️ -7s
    1 suites ±0      12 💤 ±0 
    1 files   ±0       0 ❌ ±0 

Results for commit 75cc1f4. ± Comparison against base commit 1f981a1.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 14, 2026

PyTest Results (Full)

3 829 tests  +2   3 817 ✅ +2   10m 35s ⏱️ -29s
    1 suites ±0      12 💤 ±0 
    1 files   ±0       0 ❌ ±0 

Results for commit 75cc1f4. ± Comparison against base commit 1f981a1.

♻️ This comment has been updated with latest results.

devin-ai-integration bot and others added 2 commits January 14, 2026 01:36
…yle OAuth

This adds a new configuration option to the OAuthAuthenticator that allows
excluding client credentials (client_id and client_secret) from the refresh
token request body entirely.

Some OAuth providers like Gong have unique requirements where the token
refresh endpoint only needs the refresh_token parameter and does NOT
require client credentials at all.

Changes:
- Added use_client_credentials_in_refresh field to declarative schema
- Updated abstract_oauth.py to check this option in build_refresh_request_body()
- Added use_client_credentials_in_refresh() method to DeclarativeOauth2Authenticator
- Updated ModelToComponentFactory to pass the option from manifest
- Added unit tests for the new functionality

Co-Authored-By: aldo.gonzalez@airbyte.io <aldo.gonzalez@airbyte.io>
Co-Authored-By: aldo.gonzalez@airbyte.io <aldo.gonzalez@airbyte.io>
The core fix (excluding credentials from body when Authorization header is present)
is sufficient for Gong and similar OAuth providers. The additional
use_client_credentials_in_refresh option was added during confusion about
Gong's requirements and is no longer needed.

This simplifies the implementation while maintaining the correct behavior:
- When refresh_request_headers contains an Authorization header, client_id
  and client_secret are automatically excluded from the request body
- This is required by OAuth providers like Gong that expect credentials
  ONLY in the Authorization header

Co-Authored-By: aldo.gonzalez@airbyte.io <aldo.gonzalez@airbyte.io>
Control messages for config updates (like refreshed tokens) must be printed
directly to stdout so the platform can process them immediately. Previously,
when using InMemoryMessageRepository, control messages were only queued but
never output to stdout, causing single-use refresh tokens to not be persisted.

This fix ensures emit_configuration_as_airbyte_control_message() is always
called to print to stdout, while also emitting to the message repository for
any additional processing.

Co-Authored-By: aldo.gonzalez@airbyte.io <aldo.gonzalez@airbyte.io>
@aldogonzalez8
Copy link
Copy Markdown
Contributor

Aldo Gonzalez (aldogonzalez8) commented Jan 14, 2026

/prerelease

Prerelease Job Info

This job triggers the publish workflow with default arguments to create a prerelease.

Prerelease job started... Check job output.

✅ Prerelease workflow triggered successfully.

View the publish workflow run: https://github.com/airbytehq/airbyte-python-cdk/actions/runs/21011212433

@aldogonzalez8
Copy link
Copy Markdown
Contributor

This is the connection I've been using to test https://cloud.airbyte.com/workspaces/178ce43f-7ab7-4864-8c76-07d666a203df/connections/6729d3f8-4a30-462b-9e5e-891b378b06da/timeline

Co-Authored-By: aldo.gonzalez@airbyte.io <aldo.gonzalez@airbyte.io>
@aldogonzalez8 Aldo Gonzalez (aldogonzalez8) changed the title fix(oauth): exclude client credentials from body when Authorization header is present fix(oauth): exclude client credentials from body when Authorization header is present and _emit_control_message Jan 15, 2026
Copy link
Copy Markdown
Contributor

@brianjlai Brian Lai (brianjlai) left a comment

Choose a reason for hiding this comment

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

I mainly have some questions around the part that aldo very sensibly also pointed out in the PR. The main thing we should confirm is what happens when the platform receives the CONTROL message. Once we have that confirmed, can ✅

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Control Message Flow for Single-Use Refresh Tokens

This PR fixes a critical issue with single-use refresh tokens (like Gong's). Here's a detailed explanation of the behavior for future reference.

The Problem

When a connector refreshes OAuth tokens, it receives a new refresh token from the OAuth provider. This new token must be persisted by the platform, otherwise subsequent operations will fail because the old refresh token is now invalid.

The bug: Control messages were being queued in InMemoryMessageRepository but never printed to stdout, so the platform couldn't see them.

Flow Diagrams

Manual Setup Flow (WITHOUT fix) - FAILS

┌─────────┐     ┌───────────┐     ┌──────────────┐     ┌──────────┐
│  User   │     │  Platform │     │  Connector   │     │   Gong   │
└────┬────┘     └─────┬─────┘     └──────┬───────┘     └────┬─────┘
     │                │                   │                  │
     │ Save config    │                   │                  │
     │ (client_id,    │                   │                  │
     │  client_secret,│                   │                  │
     │  refresh_token)│                   │                  │
     │───────────────>│                   │                  │
     │                │                   │                  │
     │                │ CHECK operation   │                  │
     │                │──────────────────>│                  │
     │                │                   │                  │
     │                │                   │ Refresh token    │
     │                │                   │ (no access_token)│
     │                │                   │─────────────────>│
     │                │                   │                  │
     │                │                   │ New access_token │
     │                │                   │ + NEW refresh_token
     │                │                   │<─────────────────│
     │                │                   │                  │
     │                │                   │ Queue control msg│
     │                │                   │ (InMemoryRepo)   │
     │                │                   │ ❌ NOT printed   │
     │                │                   │    to stdout!    │
     │                │                   │                  │
     │                │ CHECK passes      │                  │
     │                │<──────────────────│                  │
     │                │                   │                  │
     │                │ "Checking for     │                  │
     │                │  optional control │                  │
     │                │  message..."      │                  │
     │                │ ❌ Nothing found! │                  │
     │                │                   │                  │
     │                │ SYNC operation    │                  │
     │                │──────────────────>│                  │
     │                │                   │                  │
     │                │                   │ Refresh token    │
     │                │                   │ (OLD token!)     │
     │                │                   │─────────────────>│
     │                │                   │                  │
     │                │                   │ ❌ 401 Invalid   │
     │                │                   │<─────────────────│
     │                │                   │                  │
     │                │ ❌ SYNC FAILS     │                  │
     │                │<──────────────────│                  │
     │                │                   │                  │

Manual Setup Flow (WITH fix) - WORKS

┌─────────┐     ┌───────────┐     ┌──────────────┐     ┌──────────┐
│  User   │     │  Platform │     │  Connector   │     │   Gong   │
└────┬────┘     └─────┬─────┘     └──────┬───────┘     └────┬─────┘
     │                │                   │                  │
     │ Save config    │                   │                  │
     │───────────────>│                   │                  │
     │                │                   │                  │
     │                │ CHECK operation   │                  │
     │                │──────────────────>│                  │
     │                │                   │                  │
     │                │                   │ Refresh token    │
     │                │                   │─────────────────>│
     │                │                   │                  │
     │                │                   │ New access_token │
     │                │                   │ + NEW refresh_token
     │                │                   │<─────────────────│
     │                │                   │                  │
     │                │                   │ ✅ Print control │
     │                │                   │    msg to stdout │
     │                │                   │                  │
     │                │ CHECK passes      │                  │
     │                │<──────────────────│                  │
     │                │                   │                  │
     │                │ "Checking for     │                  │
     │                │  optional control │                  │
     │                │  message..."      │                  │
     │                │ ✅ Found! Update  │                  │
     │                │    config with    │                  │
     │                │    new tokens     │                  │
     │                │                   │                  │
     │                │ SYNC operation    │                  │
     │                │──────────────────>│                  │
     │                │                   │                  │
     │                │                   │ Use access_token │
     │                │                   │ (still valid)    │
     │                │                   │─────────────────>│
     │                │                   │                  │
     │                │                   │ ✅ 200 OK        │
     │                │                   │<─────────────────│
     │                │                   │                  │
     │                │ ✅ SYNC SUCCEEDS  │                  │
     │                │<──────────────────│                  │
     │                │                   │                  │

OAuth Flow Timing

The OAuth flow (via "Authenticate" button) delays this issue because:

  1. Platform exchanges auth code for fresh tokens (access_token + refresh_token)
  2. First few syncs use the valid access_token (no refresh needed)
  3. Issue only appears when access_token expires and refresh occurs

Key Technical Details

  • Message Repository: The factory defaults to InMemoryMessageRepository (line 692 in model_to_component_factory.py)
  • The Fix: Always call emit_configuration_as_airbyte_control_message() to print to stdout, regardless of message repository type
  • Platform Detection: Platform looks for control messages in stdout with "Checking for optional control message..."

Testing Evidence

Side-by-side testing confirmed:

  • OLD behavior (CDK 7.6.3.post5): Manual setup fails with "Refresh token is invalid or expired"
  • NEW behavior (CDK 7.6.3.post6): Both flows work correctly

@aldogonzalez8 Aldo Gonzalez (aldogonzalez8) merged commit d85cb1c into main Jan 15, 2026
44 of 46 checks passed
@aldogonzalez8 Aldo Gonzalez (aldogonzalez8) deleted the devin/1768348770-oauth-basic-auth-header branch January 15, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants