Skip to content

fix(auth): redact OAuth URL in user-facing login output - #395

Merged
ndycode merged 1 commit into
mainfrom
fix/oauth-url-redaction
Apr 17, 2026
Merged

fix(auth): redact OAuth URL in user-facing login output#395
ndycode merged 1 commit into
mainfrom
fix/oauth-url-redaction

Conversation

@ndycode

@ndycode ndycode commented Apr 17, 2026

Copy link
Copy Markdown
Owner

Summary

Redacts the live OAuth authorization URL in user-facing login output so sensitive query parameters (state, code, code_challenge, code_verifier) no longer leak to stdout, shell history, screen captures, or CI transcripts.

Problem

The browser-fallback and manual-paste login paths in lib/codex-manager.ts previously echoed the full authorization URL to console.log:

console.log(`${stylePromptText(UI_COPY.oauth.goTo, "accent")} ${url}`);

The URL contains short-lived CSRF / PKCE-binding material that must not outlive the auth flow. In practice it was captured by:

  • PowerShell / bash history
  • Terminal scrollback / screenshots
  • CI run transcripts when users pasted failures
  • Clipboard managers (the fallback path also copies the URL to clipboard)

Fix

The redactOAuthUrlForLog() helper already exists in lib/auth/auth.ts. This PR:

  1. Imports it into lib/codex-manager.ts
  2. Creates a displayUrl = redactOAuthUrlForLog(url) for every user-facing print
  3. Preserves the original url for openBrowserUrl(url) and copyTextToClipboard(url) so sign-in continues to work end-to-end

Redacted example:

Go to: https://auth.openai.com/oauth/authorize?client_id=app_EMoamEEZ73f0CkXaXp7hrann&response_type=code&state=<redacted>&code_challenge=<redacted>&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&scope=openid+profile+email+offline_access

Test changes

test/codex-manager-cli.test.ts previously mocked ../lib/auth/auth.js without exporting redactOAuthUrlForLog. The mock now includes a faithful redaction implementation so 12 existing tests that assert on the printed URL continue to pass with the sanitized surface.

lib/auth/auth.ts already has a describe("redactOAuthUrlForLog", ...) suite covering the helper itself — no changes needed to the helper or its tests.

Verification

  • Full suite: 225/225 files, 3418/3418 tests pass
  • npm run typecheck exit 0
  • npm run lint exit 0
  • No new dependencies, no public API changes
  • 0 as any, 0 @ts-ignore added
  • redactOAuthUrlForLog is existing exported API, now consumed in one additional place

Audit reference

  • docs/audits/MASTER_AUDIT.md §5 (HIGH) AUDIT-H4
  • docs/audits/evidence/dim-C-auth.md C-AUTH-05

Scope guarantees

  • ✅ One concern per commit — OAuth URL redaction in user-facing output
  • ✅ Browser opener + clipboard still receive the full URL (sign-in unbroken)
  • ✅ No behavioral change to the OAuth flow itself
  • ✅ Existing redactOAuthUrlForLog() helper re-used (no duplicate logic)
  • ✅ Test mock updated to mirror real function — not a test behavioral change

Follow-up

Phase 1 continues with PR-C (RedirectURI SSOT refactor — R2) which will address the localhost vs 127.0.0.1 drift (AUDIT-H5) and consolidate the duplicated 1455 literal across 4+ sites (AUDIT-M14 / AUDIT-M30). Tracked in .sisyphus/plans/phase1-implementation.md.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr correctly wires redactOAuthUrlForLog() into both user-facing print sites in lib/codex-manager.ts, preserving the full url for openBrowserUrl and copyTextToClipboard. the production logic is sound.

the main concern is that the test fixture url ("https://auth.openai.com/mock") carries no sensitive query params, so redactOAuthUrlForLog is effectively a no-op in every existing test — meaning a regression that printed the raw url instead of displayUrl would go undetected. additionally, no test exercises the openBrowserUrl-returns-false fallback path, which is the highest-risk scenario (url printed to terminal and written to clipboard simultaneously).

Confidence Score: 4/5

production code is correct; test suite has a gap that would not catch a displayUrl/url regression in the log statements

all p2 findings — no production bug — but the two test gaps mean the security invariant introduced by this pr is unverified by the test suite. one missing test (browser-open-fails) leaves the highest-risk code path uncovered, and the no-op fixture url means redaction correctness is never asserted. scoring 4 to flag these before merge given this is a security-focused pr where test coverage of the invariant matters.

test/codex-manager-cli.test.ts — fixture url and missing browser-fallback test case

Important Files Changed

Filename Overview
lib/codex-manager.ts correctly imports and applies redactOAuthUrlForLog; full url preserved for browser opener and clipboard; production logic is clean
test/codex-manager-cli.test.ts mock redactOAuthUrlForLog implementation is faithful, but the fixture url has no sensitive params so redaction is never exercised; no test covers the openBrowserUrl=false fallback path or asserts raw params are absent from logs

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[createAuthorizationFlow] --> B[url: full OAuth URL]
    B --> C[redactOAuthUrlForLog url]
    C --> D[displayUrl: params replaced]
    B --> E{signInMode}
    D --> E
    E -->|browser| F[openBrowserUrl url]
    F -->|opened=true| G[log success only]
    F -->|opened=false| H[console.log displayUrl]
    H --> I[copyTextToClipboard url]
    E -->|manual| J[console.log displayUrl]
    J --> K[copyTextToClipboard url]
Loading

Comments Outside Diff (1)

  1. test/codex-manager-cli.test.ts, line 82-86 (link)

    P2 browser-open-fails path has zero coverage

    openBrowserUrl is always mocked to return true (or not called at all) across every test. lines 1841-1851 in codex-manager.ts — the fallback where the url is both printed to the terminal and copied to clipboard — are never reached. that branch is the highest-risk case: it's the one scenario where a leaking raw url would appear in terminal output while simultaneously landing in a clipboard manager.

    add a test that mocks openBrowserUrl returning false, then asserts the redacted url is logged and the full url reaches copyTextToClipboard.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: test/codex-manager-cli.test.ts
    Line: 82-86
    
    Comment:
    **browser-open-fails path has zero coverage**
    
    `openBrowserUrl` is always mocked to return `true` (or not called at all) across every test. lines 1841-1851 in `codex-manager.ts` — the fallback where the url is both printed to the terminal _and_ copied to clipboard — are never reached. that branch is the highest-risk case: it's the one scenario where a leaking raw url would appear in terminal output while simultaneously landing in a clipboard manager.
    
    add a test that mocks `openBrowserUrl` returning `false`, then asserts the redacted url is logged and the full url reaches `copyTextToClipboard`.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: test/codex-manager-cli.test.ts
Line: 66-78

Comment:
**redaction is never exercised in tests — regression-blind**

every `createAuthorizationFlow` mock resolves with `url: "https://auth.openai.com/mock"` — no query params. `redactOAuthUrlForLog` returns the input unchanged, so a regression that swapped `displayUrl` back to `url` in the `console.log` call would pass all 225 tests silently.

add sensitive params to the fixture url and assert on both sides of the contract:

```ts
// fixture url with real-looking sensitive params
url: "https://auth.openai.com/oauth/authorize?client_id=x&state=csrf-secret&code_challenge=pkce-secret&code_challenge_method=S256",

// after calling runCodexMultiAuthCli:
expect(
  renderedLogs.some((l) => l.includes("state=%3Credacted%3E") || l.includes("state=<redacted>"))
).toBe(true);
expect(renderedLogs.every((l) => !l.includes("csrf-secret"))).toBe(true);
expect(renderedLogs.every((l) => !l.includes("pkce-secret"))).toBe(true);
// clipboard/browser still get the raw url
expect(vi.mocked(copyTextToClipboard)).toHaveBeenCalledWith(
  expect.stringContaining("csrf-secret")
);
```

this is the only guard that would catch a `url` vs `displayUrl` mix-up on the log lines.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: test/codex-manager-cli.test.ts
Line: 82-86

Comment:
**browser-open-fails path has zero coverage**

`openBrowserUrl` is always mocked to return `true` (or not called at all) across every test. lines 1841-1851 in `codex-manager.ts` — the fallback where the url is both printed to the terminal _and_ copied to clipboard — are never reached. that branch is the highest-risk case: it's the one scenario where a leaking raw url would appear in terminal output while simultaneously landing in a clipboard manager.

add a test that mocks `openBrowserUrl` returning `false`, then asserts the redacted url is logged and the full url reaches `copyTextToClipboard`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(auth): redact OAuth URL in user-faci..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

The browser-fallback and manual-paste login paths previously echoed the
full authorization URL — including the live 'state' and 'code_challenge'
query parameters — to stdout (and, on fallback, to the clipboard via the
user's own copy). That exposed short-lived CSRF / PKCE-binding material
to shell history, screen captures, and CI transcripts for longer than
the auth flow itself needed.

This change routes the URL through the existing redactOAuthUrlForLog()
helper for display only. The browser opener and clipboard copy continue
to receive the full URL so sign-in still works end-to-end. The test
mock for '../lib/auth/auth.js' now mirrors the real redaction so
existing assertions about the printed URL continue to reflect the
sanitized surface.

Closes AUDIT-H4 / C-AUTH-05 (live OAuth URL leak to stdout/clipboard
identified in master repository audit).

Evidence: 225/225 test files, 3418/3418 tests pass. typecheck + lint
exit 0. No new dependencies, no public API changes.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@ndycode has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 25 minutes and 11 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 25 minutes and 11 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6803688d-09c4-438a-9fec-4c21ddc504a0

📥 Commits

Reviewing files that changed from the base of the PR and between 1f6da97 and d94a4c8.

📒 Files selected for processing (2)
  • lib/codex-manager.ts
  • test/codex-manager-cli.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/oauth-url-redaction
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/oauth-url-redaction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +66 to +78
redactOAuthUrlForLog: vi.fn((url: string) => {
try {
const parsed = new URL(url);
for (const key of ["state", "code", "code_challenge", "code_verifier"]) {
if (parsed.searchParams.has(key)) {
parsed.searchParams.set(key, "<redacted>");
}
}
return parsed.toString();
} catch {
return url;
}
}),

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 redaction is never exercised in tests — regression-blind

every createAuthorizationFlow mock resolves with url: "https://auth.openai.com/mock" — no query params. redactOAuthUrlForLog returns the input unchanged, so a regression that swapped displayUrl back to url in the console.log call would pass all 225 tests silently.

add sensitive params to the fixture url and assert on both sides of the contract:

// fixture url with real-looking sensitive params
url: "https://auth.openai.com/oauth/authorize?client_id=x&state=csrf-secret&code_challenge=pkce-secret&code_challenge_method=S256",

// after calling runCodexMultiAuthCli:
expect(
  renderedLogs.some((l) => l.includes("state=%3Credacted%3E") || l.includes("state=<redacted>"))
).toBe(true);
expect(renderedLogs.every((l) => !l.includes("csrf-secret"))).toBe(true);
expect(renderedLogs.every((l) => !l.includes("pkce-secret"))).toBe(true);
// clipboard/browser still get the raw url
expect(vi.mocked(copyTextToClipboard)).toHaveBeenCalledWith(
  expect.stringContaining("csrf-secret")
);

this is the only guard that would catch a url vs displayUrl mix-up on the log lines.

Prompt To Fix With AI
This is a comment left during a code review.
Path: test/codex-manager-cli.test.ts
Line: 66-78

Comment:
**redaction is never exercised in tests — regression-blind**

every `createAuthorizationFlow` mock resolves with `url: "https://auth.openai.com/mock"` — no query params. `redactOAuthUrlForLog` returns the input unchanged, so a regression that swapped `displayUrl` back to `url` in the `console.log` call would pass all 225 tests silently.

add sensitive params to the fixture url and assert on both sides of the contract:

```ts
// fixture url with real-looking sensitive params
url: "https://auth.openai.com/oauth/authorize?client_id=x&state=csrf-secret&code_challenge=pkce-secret&code_challenge_method=S256",

// after calling runCodexMultiAuthCli:
expect(
  renderedLogs.some((l) => l.includes("state=%3Credacted%3E") || l.includes("state=<redacted>"))
).toBe(true);
expect(renderedLogs.every((l) => !l.includes("csrf-secret"))).toBe(true);
expect(renderedLogs.every((l) => !l.includes("pkce-secret"))).toBe(true);
// clipboard/browser still get the raw url
expect(vi.mocked(copyTextToClipboard)).toHaveBeenCalledWith(
  expect.stringContaining("csrf-secret")
);
```

this is the only guard that would catch a `url` vs `displayUrl` mix-up on the log lines.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

@ndycode
ndycode merged commit abe782e into main Apr 17, 2026
1 of 2 checks passed
ndycode added a commit that referenced this pull request Apr 17, 2026
…-001)

PR #395 redacted OAuth URL query params in user-facing output but opaque
refresh and access tokens can still leak via log messages when raw HTTP
token-endpoint response bodies are concatenated into logError calls.

The logger's TOKEN_PATTERNS recognises JWT (eyJ...), long hex, sk-* api
keys, and Bearer substrings, but ChatGPT refresh tokens are opaque
strings that do NOT match those patterns. Key-based redaction only
fires when a token sits under a SENSITIVE_KEYS object key, not when it
is interpolated into a free-form message string.

Identified leak sites:
- lib/auth/auth.ts exchangeAuthorizationCode: logError uses raw
  response body text after a failed /oauth/token request; the same
  text is returned verbatim as TokenResult.message which downstream
  code (codex-manager.ts logError paths) also writes to log output.
- lib/auth/auth.ts refreshAccessToken: same pattern on refresh failure.

Fix (targeted, no logger hot-path change):
- Add sanitizeOAuthResponseBodyForLog helper in lib/auth/auth.ts.
  It JSON-parses the body and masks sensitive keys (refresh_token,
  access_token, id_token, camelCase variants, token, code,
  code_verifier). On non-JSON bodies it falls back to regex scrubs
  for JSON-style key:value pairs and urlencoded key=value pairs.
- Route failed-refresh and failed-exchange bodies through the helper
  both into the logError message AND into the returned
  TokenResult.message so downstream consumers also see the sanitized
  form.

Regression tests in test/auth.test.ts assert:
- sanitizeOAuthResponseBodyForLog masks nested / camelCase /
  urlencoded / malformed-JSON token values while preserving
  non-sensitive fields and plain text.
- refreshAccessToken and exchangeAuthorizationCode never pass the
  opaque token string into logError.mock.calls, and the returned
  TokenResult.message does not contain the opaque token value.
- Non-sensitive content (error code, error_description) still
  reaches the log message.

Verification:
- npm run typecheck: clean
- npm run lint: clean
- npm test: 232 files / 3539 tests pass
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