Skip to content

feat(connectors): OAuth authorize/callback flow + auto-refresh resolver (#9019) - #10087

Merged
mrveiss merged 10 commits into
Dev_new_guifrom
issue-9019
Jun 14, 2026
Merged

mrveiss merged 10 commits into
Dev_new_guifrom
issue-9019

Conversation

@mrveiss

@mrveiss mrveiss commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Thinking Path

#9019 is the connector-group architecture gate (OAuth tokens → secrets management). Investigation found the credential-storage core already merged (PR #9079: ConnectorCredentialStore + API CRUD wiring + SecretType.OAUTH_REFRESH_TOKEN + sensitive-field registry). Two ACs remained un-met and were rescoped by the accepted ADR-007 into generic, provider-agnostic plumbing: an OAuth authorize/callback flow (AC#3) and access-token auto-refresh (AC#2). This PR delivers that plumbing so the data-source connectors (#9003/#9004/#9011) can consume it — credentials never touch Redis or the frontend.

What Changed

Backend

  • knowledge/connectors/oauth_flow.py — provider registry (Google/Microsoft/GitLab), PKCE (S256), authorize-URL builder, code exchange, refresh-token rotation. OAuth app client creds from operator config.
  • ConnectorCredentialStore.store_oauth + get_access_token — persist a self-contained token bundle via SecretsService; return a valid access token, refreshing + rotating the secret in place within an expiry skew (honors refresh-token rotation, e.g. GitLab).
  • api/knowledge_connector_oauth.py — POST .../oauth/{provider}/authorize and GET .../oauth/callback, mounted in core_routers.
  • ssot_config AuthConfig — per-provider *_oauth_client_id/secret settings (empty ⇒ provider disabled).

Security (both flagged by automated review, fixed here)

  • CSRF / token-attachment: authorize sets an HttpOnly, SameSite=Lax, path-scoped state cookie; callback requires it to match state before consuming it (GETDEL single-use), binding the completing browser to the initiator.
  • XSS: result page HTML-escapes the reflected error and JS-escapes the embedded JSON (<,>,&,U+2028/9).
  • Redirect host allowlisted via existing AUTOBOT_SSO_CALLBACK_HOSTS; HTTPS enforced via AUTOBOT_ENFORCE_HTTPS_CALLBACKS.

Frontend

Docs

  • ADR-007 extended with the flow, auto-refresh, and security properties.

Acceptance criteria (#9019)

AC Status
SecretType.OAUTH_REFRESH_TOKEN ✅ merged (#9079)
Resolver with auto-refresh ✅ get_access_token (this PR)
OAuth callback stores tokens via secrets API ✅ this PR
Plugins reference secrets by id, not env vars ✅ merged (#9079)
Token revocation removes the secret ✅ merged (#9079)
LLC company-scoped connector secrets ⏭️ ADR marks as future work (org-scope opt-in)

Verification

  • Backend: pytest knowledge/connectors/tests/{test_oauth_flow,test_credential_store,test_connector_oauth_api}.py → 41 passed (incl. CSRF-binding + XSS escaping + refresh/rotate).
  • Frontend: vitest on ConnectorOAuthButton.spec.ts + KnowledgeRepository.connectors.test.ts → 17 passed.
  • No regressions in gdrive_test.py / registry_public_api_test.py.

Model Used

Claude Opus 4.8 (1M context)

mrveiss and others added 2 commits June 13, 2026 22:12
Establishes the architectural decision that connector credentials (OAuth
refresh tokens, API keys, client secrets) must be stored via SecretsService
with Fernet encryption, not as plaintext in Redis connector config dicts.

- docs/adr/007-connector-oauth-token-storage.md — new ADR
- docs/adr/_index.md — register ADR-007 in the index
- docs/connectors/writing-a-connector.md — credential storage section with
  auth type table, sensitive-field contract, and testing guidance

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…er (#9019)

Extend the ADR-007 connector-credential gate with the generic OAuth plumbing
its data-source connectors consume:

- oauth_flow.py: provider registry (google/microsoft/gitlab), PKCE (S256),
  authorize-URL builder, code exchange, and refresh-token rotation. OAuth app
  client creds come from operator config (AuthConfig).
- ConnectorCredentialStore.store_oauth + get_access_token: persist a
  self-contained token bundle via SecretsService and return a valid access
  token, refreshing + rotating the secret in place when within the expiry skew.
- knowledge_connector_oauth.py: POST .../oauth/{provider}/authorize and GET
  .../oauth/callback. Security: state+PKCE in knowledge Redis (GETDEL single-use),
  host-allowlisted redirect, HttpOnly SameSite=Lax state cookie binding the
  completing browser (CSRF/token-attachment defense), and an HTML/JS-escaped
  result page (XSS-safe). Mounted in core_routers.
- ssot_config: per-provider OAuth client_id/secret settings.
- Frontend: KnowledgeRepository.startConnectorOAuth + reusable
  ConnectorOAuthButton.vue (popup + origin-checked postMessage) for connector
  issues (#9003/#9004/#9011) to drop into their config step.
- ADR-007: document the flow, auto-refresh, and security properties.

Tests: oauth_flow (12), credential_store oauth (8 new), endpoints (9 incl.
CSRF-binding + XSS), frontend repo + component (7). Credentials never touch
Redis or the frontend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

Applied Black, isort, and autoflake to match code-quality checks.
Triggered by workflow auto-fix-formatting.yml.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@mrveiss mrveiss closed this Jun 13, 2026
@mrveiss mrveiss reopened this Jun 13, 2026
Comment thread autobot-backend/api/knowledge_connector_oauth.py Fixed
mrveiss and others added 7 commits June 13, 2026 23:41
… label (#9019)

Bandit B106 (hardcoded_password_funcarg) false-positives on kwargs whose name
contains 'token'/'secret' with a string literal: token_url=<provider URL> and
secret_type='connector_oauth_token'. These are endpoint URLs and a SecretType
label, not credentials. Annotated inline per repo convention (cf. secret.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…th (#9019)

CodeQL py/reflective-xss (HIGH) flagged the OAuth callback result page because
the postMessage payload went through a custom _js_safe_json() sanitizer that the
analyzer cannot verify. Restructure so every attacker-influenced value reaches
the response ONLY through html.escape (a CodeQL-recognized XSS sanitizer):

- visible status: html.escape (unchanged)
- postMessage payload: json.dumps -> html.escape -> data-* attribute; the inline
  script reads it back via JSON.parse(getAttribute(...)). No user-influenced
  value is interpolated into the <script> body, so nothing can break out of the
  HTML or script context.

Removes the now-unused _js_safe_json helper. Behavior unchanged; all 9
test_connector_oauth_api tests pass incl. test_callback_escapes_reflected_error_xss.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants