Skip to content

Reject non-plaintext credential tokens before pool entry - #49

Merged
maiphucgiang merged 6 commits into
mainfrom
fix/reject-encrypted-auth-tokens
Sep 29, 2026
Merged

maiphucgiang merged 6 commits into
mainfrom
fix/reject-encrypted-auth-tokens

Conversation

@maiphucgiang

@maiphucgiang maiphucgiang commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Changes

  • Add ensure_plaintext_tokens / AuthTokenTypeError in app/auth_oauth.py: accessToken / refreshToken must be plaintext strings whenever present, with an explicit error pointing operators to OAuth login for encrypted envelope files.
  • Reject non-string values under every token alias (accessToken, access_token, token, refreshToken, refresh_token) in validate_cred_data, so an envelope refreshToken or alias can no longer pass import validation and overwrite a healthy credential the pool would later reject.
  • Enforce the guard in CredentialManager._session, so summary, header builds and refresh all fail fast instead of interpolating a non-string object into Authorization and burning the credential on upstream 401s.
  • Keep envelope files out of the credential pool: CredentialPool.reload skips them with a one-time [cred] audit failure event. Suppression is keyed to a content digest, so a different invalid credential reusing a path warns again while unchanged content stays silent, and prune() drops entries for vanished paths.
  • Evict pooled entries whose files are externally replaced by envelopes, purging ledger, cooldown, sticky and sync state so the stale identity cannot linger or block a valid repair under another filename.
  • Retry configured --auth-file paths after rejection or eviction: explicit mode remembers every configured path and rescans that list, so repairing the file re-enters the pool without a restart.
  • No configuration, dependency, schema or version changes (1.3.1).

Verification

  • Regressions in tests/test_credential_runtime.py and tests/test_auth_oauth.py: envelope tokens never enter the pool and log once per distinct content, an envelope-only refreshToken is rejected, a replaced file at a reused path warns again, a swapped pooled file evicts its entry and the next scan rejects it once, rejected and evicted explicit paths recover after repair, envelope values under every token field and alias are rejected before persistence while string aliases still pass, and ensure_plaintext_tokens accepts strings and missing fields and rejects dict/number/list/bool values.
  • Full backend suite: 1390 tests and 3889 subtests pass (2 existing conditional skips).
  • Local deployment on the live instance across all four commits: 8 credentials and 31 models unchanged after each restart; an envelope probe is refused from the pool with an audit failure event, repeat syncs do not re-admit it, and a different invalid credential at the same path warns again; importing a plaintext accessToken paired with an envelope refreshToken returns 400 and never reaches the pool; a pooled probe replaced by an envelope at the same path has its stale entry evicted on the next scan; zero-multiplier deepseek-v4.1-flash chats return 200 against the real upstream throughout.

Encrypted envelope objects in accessToken/refreshToken now fail fast in
CredentialManager instead of leaking into upstream Authorization headers;
files carrying them are kept out of the credential pool with a one-time
audit failure event, and the existing import validation keeps rejecting
them before persistence.

@sourcery-ai sourcery-ai Bot 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.

Sorry @maiphucgiang, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 18 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

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

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T22:29:14.163441Z e301825 New commits
🔒 Security Review ✅ Completed 2026-09-28T21:43:03.893380Z db58615 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 commented Sep 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

通过统一的 token 类型守卫,在凭据会话使用和凭据池加载两个边界阻止非明文 token 泄漏到上游;非法文件被隔离并仅记录一次审计失败,同时新增覆盖导入、入池、重复扫描和文件替换场景的回归测试。

Sequence diagram for plaintext token validation and credential pool loading

sequenceDiagram
    participant Pool as CredentialPool
    participant Manager as CredentialManager
    participant Auth as auth_oauth
    participant Audit as AuditLog
    participant Upstream as UpstreamAPI

    Pool->>Manager: summary()
    Manager->>Manager: _session()
    Manager->>Auth: ensure_plaintext_tokens(auth)
    alt accessToken or refreshToken is non-string
        Auth-->>Manager: AuthTokenTypeError
        Manager-->>Pool: reject credential
        Pool->>Audit: _log(failure)
        Pool-->>Pool: remember cid in _ignored_invalid
    else plaintext tokens
        Auth-->>Manager: validation succeeds
        Manager-->>Pool: credential summary
        Pool->>Upstream: use credential headers
    end
Loading

File-Level Changes

Change Details Files
新增统一的明文 token 类型校验,阻止加密信封等非字符串值进入上游请求头。
  • 新增 AuthTokenTypeError 和 ensure_plaintext_tokens,分别校验 accessToken 与 refreshToken。
  • 在凭据会话加载后强制校验,使 summary、请求头生成和 refresh 等使用路径快速失败。
app/auth_oauth.py
converter.py
在凭据池重载阶段拒绝包含非法 token 的文件,并提供去重后的审计告警。
  • 将类型校验失败的凭据排除出池,不影响其他有效凭据。
  • 通过已拒绝集合避免重复扫描产生重复告警,凭据恢复有效后清除拒绝状态。
converter.py
补充导入校验、凭据池隔离、重复告警抑制及运行时替换场景的回归覆盖。
  • 验证 accessToken 或仅 refreshToken 为信封时均被拒绝。
  • 验证已入池文件被替换后下一次使用快速失败,且不会阻塞其他凭据。
  • 验证导入路径继续拒绝信封凭据。
tests/test_auth_oauth.py
tests/test_credential_runtime.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db586150d3

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/auth_oauth.py
@maiphucgiang maiphucgiang changed the title fix: 拒绝非明文 token 凭据入池,避免加密信封泄漏到上游请求头 Reject non-plaintext credential tokens before pool entry Sep 28, 2026
validate_cred_data now refuses non-string values under accessToken,
access_token, token, refreshToken and refresh_token before persistence,
so an envelope refreshToken can no longer overwrite a healthy credential
that the pool would later reject as unusable.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6c6d3ecb1

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread converter.py Outdated
Comment on lines +587 to +589
if cid not in self._ignored_invalid:
_log(f"[cred] 拒绝入池(入库校验失败:{error}): {Path(cid).name}")
self._ignored_invalid.add(cid)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prune invalid-path suppression after file removal

When scan mode observes an invalid credential being deleted and a later invalid credential reuses the same pathname, this process-lifetime set still contains cid, so the new rejection produces no audit log. Unlike _ignored_duplicates, prune() never removes nonexistent paths from _ignored_invalid; this also lets the set grow without bound in directories where invalid credential filenames churn. Prune this set alongside the duplicate-path set so suppression applies only to repeated scans of the same file.

Useful? React with 👍 / 👎.

prune() now drops _ignored_invalid entries for missing paths like the
duplicate set, so a recreated invalid file at a reused path warns again
and the set cannot grow without bound.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3bc729981

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/auth_oauth.py

def ensure_plaintext_tokens(auth: dict) -> None:
"""Reject non-string token fields so encrypted envelope objects never reach upstream headers."""
for field in ("accessToken", "refreshToken"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard token aliases during direct directory scans

Although import validation now checks aliases, the documented direct-copy path scans .info files without calling validation or normalization, and this runtime guard checks only canonical names. Consequently, a file with a plaintext accessToken and object-valued refresh_token is admitted to the pool; it serves until refresh, when _refresh_locked() reads the absent refreshToken and sends an empty refresh header instead of rejecting and auditing the envelope. Include access_token, token, and refresh_token here (or normalize scanned credentials before summary()).

Useful? React with 👍 / 👎.

A replaced invalid credential at a reused path can share inode and
mtime with the file it replaced, so path-presence pruning alone missed
it; suppression now compares a content digest, warns once per distinct
file and stays silent across rescans of unchanged content.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a89a03925

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread converter.py
manager = CredentialManager(Path(cid))
try:
summary = manager.summary()
except auth_oauth.AuthTokenTypeError as error:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Evict existing entries that become invalid

When an already-pooled .info file is externally replaced with an envelope credential, reload() takes the existing-entry branch, whose broad exception handler returns before this new rejection logic runs. The stale entry therefore remains indefinitely with its old UID and identity, produces no rejection log, and can cause find_by_uid() to reject a valid repair imported under another filename as an account conflict. Handle AuthTokenTypeError in the existing-entry branch too and remove or invalidate that entry.

Useful? React with 👍 / 👎.

The existing-entry reload branch now treats AuthTokenTypeError like a
removal: the stale identity is dropped with its ledger, cooldown,
sticky and sync state instead of lingering without a warning or
blocking a valid repair under another filename.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4076b4a76c

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread converter.py
# reusing the path warns again even without a scan while missing.
_log(f"[cred] 拒绝入池(入库校验失败:{error}): {Path(cid).name}")
self._ignored_invalid[cid] = digest
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain rejected explicit paths for later rescans

When the pool is constructed with --auth-file (scan=False) and a configured file contains an envelope token, this continue leaves the path out of _entries; _rescan() derives its paths solely from the remaining entries in that mode. Consequently, replacing the rejected file with a valid plaintext credential is never detected, and a sole credential remains unavailable until the process restarts. The same permanent loss occurs when an existing explicit entry is evicted by the new AuthTokenTypeError branch, so configured paths need to be tracked independently and retried.

Useful? React with 👍 / 👎.

Explicit --auth-file mode now remembers every configured path and
rescans that list, so a file rejected for envelope tokens or an entry
evicted after an external replacement re-enters the pool as soon as the
file is repaired, without waiting for a process restart.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e301825fdb

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread converter.py
Comment on lines +546 to +547
if cid not in self._configured:
self._configured.append(cid)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Track only genuinely configured credential paths

When running with --auth-file (scan=False), every path passed to reload() is now permanently treated as configured, including credentials added later through the admin/OAuth import paths. If such a dynamically added credential is deleted, remove_file() never removes it from _configured, so every request continues probing that tombstone and an unrelated external file recreated under the same name is silently admitted even though explicit mode is supposed to use only configured files. Over credential churn this list and the per-request filesystem work also grow without bound; retain constructor paths separately or remove non-explicit paths on intentional deletion.

Useful? React with 👍 / 👎.

@maiphucgiang
maiphucgiang merged commit ce84c16 into main Sep 29, 2026
9 checks passed
@maiphucgiang
maiphucgiang deleted the fix/reject-encrypted-auth-tokens branch September 29, 2026 03:33
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.

1 participant