Skip to content

Auth re-prompt loop after successful provider authorization #799

Description

@rahulchhabria

Summary

After completing a provider OAuth flow in the browser (user sees "Provider account connected"), Junior re-prompts for authorization on the next turn. The browser showed success, but subsequent Sentry API calls trigger another auth link.

Confirmed facts

  • The OAuth callback saves the token synchronously before returning the success HTML. If the token write or parse fails, the browser shows a failure page — not success. So the token IS in the store when the browser renders success.
  • Re-prompting requires a fresh auth_required signal from the sandbox egress layer. That signal fires when the credential broker throws CredentialUnavailableError (token missing/expired) or when an upstream API returns 401.
  • When auth_required fires via maybeHandleAuthSignal, startAuthorizationPause is always called with unlinkExistingProvider: true. This calls unlinkProvider, which deletes the stored OAuth token before sending a new auth link.

Most likely root cause

  1. User completes Sentry OAuth → callback saves token → browser shows success.
  2. Turn resumes (or user sends a new message starting a fresh turn).
  3. Agent uses Sentry tool → sandbox egress injects the OAuth token → calls Sentry API.
  4. Sentry API returns 401 (scope gap, org/project mismatch, token audience issue, or similar).
  5. Egress records auth_required signal for the sentry provider.
  6. Auth orchestration calls startAuthorizationPause("sentry", { unlinkExistingProvider: true }).
  7. unlinkProvider deletes the freshly stored Sentry token from the user token store.
  8. New Sentry OAuth link sent → user sees another auth prompt.
  9. Loop repeats.

This means every time the Sentry API rejects the token (for any reason), the system does a destructive re-auth that wipes a potentially-valid credential rather than surfacing an actionable error.

Supporting bugs

1. Silent ResumeTurnBusyError swallow (oauth-callback.ts)

When the conversation lock is contended, ResumeTurnBusyError is caught and silently discarded with no log and no retry. The original paused turn is never resumed; the session stays stuck in awaiting_resume.

2. pendingAuth not cleared on session abandon (reply-executor.ts)

When a new user message arrives while a session is in awaiting_resume with resumeReason === "auth", the old session is abandoned and the turn is marked closed — but conversation.processing.pendingAuth is not cleared. The stale pending-auth record persists until the next successful credential issuance.

3. Related: JUNIOR-40 — GitHub OAuth refresh with HTTP 200 error body

In the GitHub plugin, refreshUserAccessToken calls github.com/login/oauth/access_token for token rotation. GitHub returns HTTP 200 for some error conditions. In edge cases where oauthErrorCode evaluates falsy (empty error field, non-JSON URL-encoded response), the check !response.ok || errorCode passes, and parseOAuthTokenResponse throws "OAuth token response missing access_token". Surfaces as an unhandled crash in the GitHub sandbox egress. See JUNIOR-40 — 33 events, 5 users, ongoing.

Alternative root causes to rule out

Before closing with the hypothesis above, confirm that Rahul's trace doesn't show:

  • Token store miss immediately after callback — would indicate user ID / provider key mismatch between callback write (stored.userId) and broker read (credentialUserSubjectId)
  • Credential not injected — would indicate the Sentry CLI request host doesn't match plugin.yaml domains (sentry.io, us.sentry.io, de.sentry.io)
  • No upstream 401 before re-prompt — would point to pending-auth/session-reuse logic as root cause

Fix direction

Primary: Stop unconditionally deleting provider credentials on every auth_required signal. Differentiate:

  • Token-store miss / no credential available → re-auth makes sense
  • Upstream 401 after credential injection → likely scope/org mismatch; surface a specific error; do not unlink until the token is confirmed invalid or user explicitly requests reconnect

Secondary:

  • Log ResumeTurnBusyError instead of silently swallowing it; consider a retry mechanism
  • Clear pendingAuth when abandoning an awaiting_resume auth session
  • Add structured audit logs (provider, user, session, token-store hit/miss, upstream status) to make re-auth loops diagnosable

Evidence

  • Reported in #proj-junior
  • Related Sentry event: JUNIOR-40 — GitHub OAuth token refresh crash, ongoing

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions