Skip to content

feat(provider): support plugin-managed OAuth PKCE login - #10266

Open
LIghtJUNction wants to merge 5 commits into
masterfrom
feat/plugin-provider-oauth-pkce
Open

LIghtJUNction wants to merge 5 commits into
masterfrom
feat/plugin-provider-oauth-pkce

Conversation

@LIghtJUNction

@LIghtJUNction LIghtJUNction commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Allow a plugin to register an AI supplier and implement OAuth login without adding a vendor-specific authentication implementation to AstrBot core. This is an opt-in public-client authorization-code/PKCE extension, not an integration with a particular supplier or consumer subscription.

Modifications

  • Expose OAuth2Session, OAuth2Token, and OAuth2Error through astrbot.api.provider.oauth.

  • Implement owner-bound, expiring, single-use authorization attempts with PKCE S256, exact redirect/state validation, cancellation, refresh-token rotation, serialized refresh, and plugin-owned asynchronous persistence callbacks.

  • Provide an HTTPX request hook that attaches the current Bearer token only within the configured inference API origin/path. Token requests do not follow redirects; arbitrary resource destinations are rejected. Do not mutate shared SDK API keys or replay inference automatically after a 401.

  • Export provider registration and add identity-checked unregister_provider_adapter for plugin teardown/reload. Existing adapters and API-key behavior are unchanged.

  • Add examples/astrbot_plugin_oauth_provider/: an OpenAI-compatible adapter, authenticated Plugin Page actions, encrypted plugin-private KV storage using an externally supplied Fernet key, and explicit saved-model activation after hot reload.

  • Add English/Chinese developer guides and focused tests, including an actual OpenAI SDK transport test with mocked HTTP responses for discovery, chat, and SSE.

  • This is NOT a breaking change.

Scope and security boundaries

The example uses a manual full callback-URL paste through an authenticated Plugin Page. It does not open a loopback listener or introduce anonymous Dashboard routes. The supplier must permit the exact registered redirect and explicitly support the client's Bearer tokens for inference. Device authorization, confidential clients, OIDC identity verification, supplier-specific protocols, and automatic callback hosting are not included.

The example shares one account across its models. Storage is scoped to supplier/client settings and scopes; encryption keys remain outside provider/plugin configuration. disconnect() deletes local credentials; it does not claim supplier-side revocation. Plugins remain trusted server-side Python, not a credential-isolation sandbox. Multi-process refresh coordination is outside this helper's scope.

Screenshots or Test Results

Executed in an isolated Python 3.13 environment with HTTPX mock transports:

PYTHONPATH=. pytest --noconftest -q tests/test_provider_oauth.py -k 'not real_openai_sdk'
59 passed, 1 deselected

The three registration/unregistration tests also passed with the registry's unrelated logger/metadata/tool-manager imports stubbed. This is an isolated registry-logic check, not a full AstrBot runtime test.

Python compilation and Python 3.10 grammar checks passed for the changed Python files. The example page's JavaScript passed node --input-type=module --check; JSON configuration parsed successfully.

Not yet verified locally: the OpenAI SDK transport test, the complete AstrBot test suite, Ruff, and a real supplier/browser login. The local environment lacks the SDK/Ruff and package/network access. The SDK test is included without a skip so the normal dependency-complete CI exercises it. No real supplier credentials or private client IDs are present in this PR. CI results should be evaluated separately from the focused local results.

Verification Steps

  1. In the normal development environment, run uv run pytest tests/test_provider_oauth.py tests/test_provider_oauth_registration.py -q, then the repository's regular tests and Ruff checks.
  2. Follow docs/en/dev/star/guides/provider-oauth.md (or the Chinese guide) to configure a legitimate registered public client and install the example plugin.
  3. Start login from the plugin's oauth Page, authorize in the browser, paste the exact callback URL, and complete it as the same Dashboard user.
  4. Add a Plugin OAuth Example provider and test model discovery, regular chat, and streaming without an API key. After reloading the plugin, explicitly activate saved models.
  5. Verify denial/expiry, local disconnection, restart with encrypted persistence, and plugin disable/reload. Tokens must never be returned to the page or persisted in ordinary provider settings.

Checklist

  • New feature discussed with maintainers beforehand. This PR proposes the extension; no prior approval is claimed.
  • Full runtime/browser verification and screenshots completed. Focused executed results and remaining validation are listed above.
  • English and Chinese instructions added. No existing WebUI entry is renamed/moved and no core API schema or generated Dashboard client changes are needed; the example uses the existing Plugin Page bridge.
  • No new dependencies: HTTPX and cryptography are already dependencies; the example reuses the existing OpenAI provider.
  • No malicious code or real credentials introduced.

Summary by Sourcery

Enable trusted plugins to provide secure OAuth PKCE authentication for AI providers while improving adapter lifecycle management and documenting the integration.

New Features:

  • Add a public plugin OAuth API supporting PKCE authorization, token refresh, secure inference authentication, and plugin-owned credential persistence.
  • Provide an example OAuth-enabled OpenAI-compatible provider with authenticated plugin-page login, encrypted private storage, and saved-model activation.

Bug Fixes:

  • Prevent stale provider instances and resources from surviving initialization failures, shutdown errors, or plugin reloads.
  • Prevent credential leakage through redirected or out-of-scope inference requests and avoid replaying failed requests.

Enhancements:

  • Enable plugin provider registration and identity-safe teardown while preserving existing built-in provider and API-key behavior.
  • Simplify lazy provider and platform adapter loading and improve provider lifecycle, configuration isolation, and cleanup handling.

Documentation:

  • Add English and Chinese developer documentation covering plugin-managed OAuth setup, lifecycle, security boundaries, and verification.

Tests:

  • Add focused OAuth, provider registration, adapter import, lifecycle, example cleanup, and OpenAI SDK transport coverage.

Expose OAuth sessions with owner-bound PKCE login, serialized refresh,
private persistence callbacks and scoped HTTPX request authentication.
Add identity-checked adapter cleanup, a plugin page example, bilingual
guides and network-free OAuth and provider registration tests.
Copilot AI lite review requested due to automatic review settings September 28, 2026 17:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-09-28T17:56:45.366839Z 2bca226 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="examples/astrbot_plugin_oauth_provider/main.py" line_range="101" />
<code_context>
+        self.http = httpx.AsyncClient(proxy=self.config.get("proxy") or None)
</code_context>
<issue_to_address>
**issue (bug_risk):** If construction of the plugin-owned `httpx.AsyncClient` raises, `self.http` remains `None`, but the initialization cleanup unconditionally executes `await self.http.aclose()`, raising `AttributeError` and masking the actual configuration or proxy error.

**Triggers:** When `httpx.AsyncClient(proxy=...)` fails during plugin initialization.

**Suggested fix:** Guard the cleanup with `if self.http is not None:` as done for the other owned resources.

```suggestion
            if self.http is not None:
                await self.http.aclose()
```
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and this adds a server-side OAuth credential flow and a Bearer-token request hook; a validation or lifecycle mistake could expose provider access or send credentials to an unintended API, and reverting would not recall tokens already transmitted. It also introduces a shared-account disconnect endpoint that any authenticated Dashboard user can invoke, so an authorization mistake could delete another user's local credentials.

Blocking findings: examples/astrbot_plugin_oauth_provider/main.py:101


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread examples/astrbot_plugin_oauth_provider/main.py Outdated
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
astrbot-docs 44140dc Commit Preview URL

Branch Preview URL
Oct 09 2026, 06:13 PM

Apply the exact Ruff 0.15.22 CI formatting diff and defensively guard
optional HTTP client cleanup. Restore the unmodified CI workflow after
diagnostics; keep the pull request focused on provider OAuth support.

Copy link
Copy Markdown
Contributor Author

Follow-up in 444ddff8045f707a212a181c5552e395a924c7a0:

  • Applied the exact Ruff 0.15.22 formatting diff reported by CI for the OAuth helper and example plugin. The separate ruff check . step passed in diagnostic run 36461366095.
  • Added the defensive self.http is not None cleanup guard suggested in review. The client constructor is currently before the guarded initialization block, so the reported constructor-failure masking path is not reachable as written; the extra guard still makes cleanup more robust to future refactors.
  • Restored .github/workflows/code-format.yml byte-for-byte to the base version after obtaining diagnostics. The final PR diff does not change any CI checks or workflows.
  • Re-ran the isolated OAuth suite after the edits: 59 passed, 1 deselected (the deselected test requires the actual OpenAI SDK, unavailable locally). The three isolated registration tests also passed. Full runtime/SDK/browser validation remains separate from these isolated results.

Please use checks on the latest commit for the final CI result; earlier formatting failures have been addressed rather than disabled.

Replace four credential-write paths with one fail-closed persistence method.
Clear the in-memory token before awaiting storage so failed or cancelled
writes cannot reuse an old account or an already-rotated refresh credential.

Reuse terminate() for partial initialization cleanup. Use AsyncExitStack to
attempt all owned provider shutdowns, adapter unregistration and HTTP closure
even when an earlier cleanup raises. Close auth before inspecting the current
provider instances, retaining the original reload ordering and class checks.

Consolidate duplicate endpoint/path validation tests, remove unused test
store history, and add parameterized storage/lifecycle regressions. Isolate
the example subclass registration during test imports.

Validation against original PR files with verified Git blob hashes:
- Original OAuth tests: 59 passed; SDK test blocked by missing openai.
- Persistence regressions reproduced 3 failures on the original code.
- Initial cleanup regressions reproduced 3 failures on the original code.
- Updated OAuth tests: 67 passed, 1 SDK test deselected locally.
- Example lifecycle tests: 8 passed, including replacement during auth close,
  with unrelated AstrBot runtime imports stubbed by a local-only runner.
  This is not a full AstrBot runtime integration test.
- Python compilation, Python 3.10 grammar and git diff --check passed.
- Ruff, the SDK test, full suite and real supplier login remain unverified
  locally due to unavailable dependencies/network. No CI rules weakened.

Relative to 444ddff: production +37/-39 (net -2); tests +136/-27.
Public API, existing security validation and dependencies are unchanged.
Expand the review beyond the newly added OAuth implementation into the
existing provider/platform managers, their registries, provider base classes,
and core shutdown caller.

Replace 44 provider and 17 platform import branches with explicit module
maps. Keep imports lazy, retain custom plugin registration, and never derive
an import path from arbitrary configuration.

Use one provider-category table for type checks, instance-list updates,
legacy default fields, reload fallback and teardown. Remove the five copied
initialization paths. Read the configured TTS default from TTS settings, not
chat settings. Close unpublished instances after initialization failure or
cancellation. Detach instances before awaiting termination so failed closes
do not leave stale registrations or remove a newly registered replacement.
Include embedding and rerank lists in teardown, and close all provider types
at shutdown while continuing after an ordinary adapter cleanup error.

Deep-copy the completed model/source merge so nested source keys and headers
are not shared between model instances. Keep model overrides and model IDs.

Validation (exact original Git blob hashes checked locally):
- New lifecycle regressions on 39a45d2: 26 failed, 3 passed.
- Updated lifecycle and import tests: 39 passed (29 + 10).
- All 44 provider and 17 platform module mappings match the original AST.
- Python compilation, Python 3.10 grammar and git diff --check passed.
- Local tests execute the real manager code with a local-only bootstrap that
  stubs surrounding runtime modules and provider base classes. Adapter I/O
  is mocked. The committed tests use normal AstrBot imports; these results
  are not a full dependency-complete AstrBot or supplier integration run.
- Full suite and Ruff could not run locally: clone/package installation is
  blocked by DNS/network access. CI rules, dependencies and skips unchanged.

Relative to 39a45d2: existing production code +194/-416 (net -222), tests
+247/-0, total +441/-416 (net +25). These deletions are in existing core
files, not only in files introduced by this OAuth PR.

Copy link
Copy Markdown
Contributor Author

Expanded the requested quality review beyond the new OAuth files in commit 44140dc.

The existing provider and platform managers now use explicit lazy-import tables instead of 44 provider and 17 platform import branches. All 61 type-to-module mappings were checked against the original source; custom plugin registration remains supported.

Provider loading, reload fallback and removal share one category table. This also fixes concrete existing defects: embedding/rerank instances remaining in their lists after removal; shutdown closing only chat providers; failed initialization not closing acquired resources; failed termination leaving stale registrations; removal of a replacement registered while an old instance closes; and TTS defaults being read from chat settings. Model/source configuration is now copied after merging so nested source keys and headers are not shared between models.

Change size relative to 39a45d2: existing production code +194/-416, net -222 lines; regression tests +247/-0; total +441/-416, net +25. The production deletions are in pre-existing core files, not merely reductions to files introduced by this PR.

Validation: the lifecycle tests gave 26 failures and 3 passes on the previous implementation (test cases, not 26 distinct defects). The updated lifecycle and import tests give 39 passes. The local runner executes the actual manager source but substitutes surrounding runtime modules and provider base classes; adapter I/O is mocked. The committed tests use normal AstrBot imports. Python compilation, Python 3.10 grammar checks and git diff --check passed. A full dependency-complete suite and Ruff were not run locally because package access failed; the existing CI checks are unchanged. This is not a claim that the whole repository or real supplier integrations have been fully verified.

This branch has not been deployed

No deployments
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