Skip to content

fix: add missing error handling in OAuth token exchange and credential decryption#148

Merged
guyb1 merged 2 commits intoonecli:mainfrom
lawrence3699:fix/oauth-token-exchange-error-handling
Apr 3, 2026
Merged

fix: add missing error handling in OAuth token exchange and credential decryption#148
guyb1 merged 2 commits intoonecli:mainfrom
lawrence3699:fix/oauth-token-exchange-error-handling

Conversation

@lawrence3699
Copy link
Copy Markdown
Contributor

Summary

Was debugging a self-hosted deployment where Google OAuth kept failing with cryptic JSON parse errors. Traced it to the token exchange endpoint returning a 503 during a brief outage — tokenRes.json() was called without checking tokenRes.ok first, so instead of a clear error, users got an unhelpful SyntaxError: Unexpected token '<' from trying to parse an HTML error page as JSON.

Noticed the same pattern in github.ts — the token exchange fetch also skips the .ok check. Interestingly, both files already do it correctly for the user info fetch right below (if (userRes.ok) in google-oauth.ts:87 and github.ts:116), so this just brings the token exchange in line with the existing pattern.

Also found that getAppConfigCredentials() in app-config-service.ts calls JSON.parse() on decrypted credentials without a try-catch. If the encrypted blob gets corrupted (wrong key rotation, truncated write, etc.), this crashes the whole credential resolution flow instead of falling back gracefully. Other files like oauth-state.ts already wrap JSON.parse in try-catch for exactly this reason.

Changes

  • google-oauth.ts: Add if (!tokenRes.ok) guard before parsing token exchange response
  • github.ts: Same — add if (!tokenRes.ok) guard before parsing token exchange response
  • app-config-service.ts: Wrap JSON.parse of decrypted credentials in try-catch, return settings-only on corruption

Test plan

  • Verify Google OAuth flow works normally (no regression)
  • Verify GitHub OAuth flow works normally
  • Simulate a non-200 token exchange response — should now throw a clear error with status code
  • Simulate corrupted encrypted credentials — should return settings without crashing

Copilot AI review requested due to automatic review settings April 2, 2026 10:55
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves resilience and debuggability of OAuth flows and credential resolution by adding explicit handling for failed token exchanges and corrupted encrypted credential blobs.

Changes:

  • Add Response.ok guards before parsing OAuth token exchange responses for Google and GitHub.
  • Wrap decrypted-credentials JSON.parse in a try/catch to avoid crashing credential resolution on corruption, falling back to settings-only.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
apps/web/src/lib/services/app-config-service.ts Prevents crashes when decrypted credentials can’t be parsed/decrypted by returning settings-only.
apps/web/src/lib/apps/google-oauth.ts Adds non-2xx guard for Google token exchange to avoid JSON parse errors on non-JSON bodies.
apps/web/src/lib/apps/github.ts Adds non-2xx guard for GitHub token exchange to avoid JSON parse errors on non-JSON bodies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +53
throw new Error(
`Google token exchange failed: ${tokenRes.status} ${tokenRes.statusText}`,
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The early !tokenRes.ok throw prevents reading Google’s JSON error payload (e.g., error_description), and makes the later tokenData.error handling ineffective for non-2xx responses. Consider on non-OK: read Content-Type and attempt tokenRes.json() when it’s JSON (falling back to tokenRes.text() otherwise) so the thrown error includes the provider’s error details while still avoiding Unexpected token '<' on HTML bodies.

Suggested change
throw new Error(
`Google token exchange failed: ${tokenRes.status} ${tokenRes.statusText}`,
const contentType = tokenRes.headers.get("content-type") ?? "";
let errorDetail: string | undefined;
try {
if (contentType.includes("application/json")) {
const errorBody = (await tokenRes.json()) as {
error?: string;
error_description?: string;
};
errorDetail =
errorBody.error_description ||
errorBody.error ||
undefined;
} else {
const text = await tokenRes.text();
errorDetail = text || undefined;
}
} catch {
// Ignore body parsing errors; fall back to generic message below.
}
const baseMessage = `Google token exchange failed: ${tokenRes.status} ${tokenRes.statusText}`;
throw new Error(
errorDetail ? `${baseMessage} - ${errorDetail}` : baseMessage,

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +93
throw new Error(
`GitHub token exchange failed: ${tokenRes.status} ${tokenRes.statusText}`,
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

On non-OK responses, this throws using only status/statusText and skips reading the response body. GitHub’s token endpoint often returns a JSON body with useful error/error_description even when the status isn’t 2xx; consider including the parsed JSON (when available) or at least await tokenRes.text() in the thrown error to improve diagnostics.

Suggested change
throw new Error(
`GitHub token exchange failed: ${tokenRes.status} ${tokenRes.statusText}`,
let errorDetail = "";
try {
const bodyText = await tokenRes.text();
if (bodyText) {
errorDetail = ` - Response body: ${bodyText}`;
}
} catch {
// Ignore body read errors; fall back to status information only.
}
throw new Error(
`GitHub token exchange failed: ${tokenRes.status} ${tokenRes.statusText}${errorDetail}`,

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +64
} catch {
// Corrupted or invalid encrypted credentials — return settings only
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

This catch swallows all decrypt/parse errors and returns settings without any logging, which can make key misconfiguration (e.g., missing/rotated SECRET_ENCRYPTION_KEY) or data corruption very hard to diagnose in production. Consider catching (err) and logging a warning/error with { accountId, provider } (without secrets) before returning settings.

Suggested change
} catch {
// Corrupted or invalid encrypted credentials — return settings only
} catch (err) {
// Corrupted or invalid encrypted credentials — log and return settings only
console.error("Failed to decrypt app config credentials", {
accountId,
provider,
error: err instanceof Error ? err.message : err,
});

Copilot uses AI. Check for mistakes.
lawrence3699 and others added 2 commits April 3, 2026 21:27
…l decryption

Check response.ok before parsing token exchange responses, and wrap
JSON.parse in credential decryption with try-catch for corrupted data.
@guyb1 guyb1 force-pushed the fix/oauth-token-exchange-error-handling branch from 24f41c0 to b61eb8f Compare April 3, 2026 18:27
@guyb1 guyb1 merged commit d9e70c6 into onecli:main Apr 3, 2026
1 check passed
@guyb1 guyb1 mentioned this pull request Apr 3, 2026
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.

3 participants