Skip to content

fix(oauth): preserve consent failures - #25

Merged
JOY (JOY) merged 1 commit into
devfrom
codex/beta-oauth-callback-fix
Aug 31, 2026
Merged

fix(oauth): preserve consent failures#25
JOY (JOY) merged 1 commit into
devfrom
codex/beta-oauth-callback-fix

Conversation

@JOY

@JOY JOY (JOY) commented Aug 31, 2026

Copy link
Copy Markdown

Problem

A failed first-party OAuth consent POST returns 401, but the global frontend handler navigates to /. On Beta, the proxy redirects / to /launches, masking the safe consent error and leaving the DOS-Me OAuth flow unused.

Change

Preserve 401 responses from /oauth/authorize on the OAuth page unless the backend explicitly requests logout. The existing authorization action then renders its static consent-session error instead of navigating away.

Validation

  • pnpm exec jest --config tests/bootstrap.jest.cjs --runInBand tests/bootstrap-oauth-consent-error.spec.ts
  • pnpm --filter ./apps/frontend exec tsc --noEmit
  • git diff --check

Note

Low Risk
Narrows an existing global 401 handler for one route; logout-header behavior on /oauth/authorize is unchanged.

Overview
Failed first-party OAuth consent POSTs no longer trigger the global 401 redirect to / (and thus away from the consent UI on environments that bounce / to /launches).

The layout fetch afterRequest handler now skips cookie clearing and window.location.href = '/' when shouldPreserveOAuthConsentUnauthorized matches: 401 on /oauth/authorize (query string ignored), with no logout response header. A small helper in oauth-consent-unauthorized.ts encodes that rule so the authorize page can show the existing safe consent-session error instead of navigating away.

Tests in bootstrap-oauth-consent-error.spec.ts cover the helper鈥檚 true/false cases (other paths, logout header).

Reviewed by Cursor Bugbot for commit 775cc83. Configure here.

@cursor

cursor Bot commented Aug 31, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e8bbc87e-6617-4f96-9dad-fadf367d67cc)

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a helper function shouldPreserveOAuthConsentUnauthorized to prevent unauthorized (401) responses on the OAuth consent page (/oauth/authorize) from triggering a global logout or redirect, and adds corresponding unit tests. The review feedback suggests improving the URL parsing to robustly handle absolute URLs using the URL API, reusing an existing local logout variable in layout.context.tsx for consistency, and adding a test case to verify absolute URL handling.

Comment on lines +1 to +11
export function shouldPreserveOAuthConsentUnauthorized(
url: string,
status: number,
hasLogoutHeader: boolean
) {
return (
!hasLogoutHeader &&
status === 401 &&
url.split('?', 1)[0] === '/oauth/authorize'
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current implementation of shouldPreserveOAuthConsentUnauthorized uses url.split('?', 1)[0] === '/oauth/authorize' to check the path. However, if the fetch wrapper resolves or prepends a base URL (e.g., https://api.gitroom.com/oauth/authorize), url will be an absolute URL, and this check will fail, causing the application to redirect to / instead of preserving the consent failure.

Using new URL(url, 'http://localhost') provides a robust way to extract the pathname for both relative and absolute URLs.

Suggested change
export function shouldPreserveOAuthConsentUnauthorized(
url: string,
status: number,
hasLogoutHeader: boolean
) {
return (
!hasLogoutHeader &&
status === 401 &&
url.split('?', 1)[0] === '/oauth/authorize'
);
}
export function shouldPreserveOAuthConsentUnauthorized(
url: string,
status: number,
hasLogoutHeader: boolean
) {
if (hasLogoutHeader || status !== 401) {
return false;
}
try {
const parsedUrl = new URL(url, 'http://localhost');
return parsedUrl.pathname === '/oauth/authorize';
} catch {
return url.split('?', 1)[0] === '/oauth/authorize';
}
}

Comment on lines +84 to +91
if (
(response.status === 401 || response?.headers?.get('logout')) &&
!shouldPreserveOAuthConsentUnauthorized(
url,
response.status,
Boolean(response?.headers?.get('logout'))
)
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The local variable logout is already defined at lines 44-45, which also handles the case-insensitive fallback (Logout). We should reuse it here instead of calling response?.headers?.get('logout') multiple times.

Suggested change
if (
(response.status === 401 || response?.headers?.get('logout')) &&
!shouldPreserveOAuthConsentUnauthorized(
url,
response.status,
Boolean(response?.headers?.get('logout'))
)
) {
if (
(response.status === 401 || logout) &&
!shouldPreserveOAuthConsentUnauthorized(
url,
response.status,
Boolean(logout)
)
) {

Comment on lines +24 to +41
it('keeps a failed consent POST on the OAuth page instead of sending it to launches', () => {
expect(
shouldPreserveOAuthConsentUnauthorized('/oauth/authorize', 401, false)
).toBe(true);
expect(
shouldPreserveOAuthConsentUnauthorized(
'/oauth/authorize?state=test',
401,
false
)
).toBe(true);
expect(
shouldPreserveOAuthConsentUnauthorized('/user/profile', 401, false)
).toBe(false);
expect(
shouldPreserveOAuthConsentUnauthorized('/oauth/authorize', 401, true)
).toBe(false);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Adding a test case for absolute URLs ensures that shouldPreserveOAuthConsentUnauthorized works correctly when the fetch wrapper prepends the base URL (e.g., in production or beta environments).

  it('keeps a failed consent POST on the OAuth page instead of sending it to launches', () => {
    expect(
      shouldPreserveOAuthConsentUnauthorized('/oauth/authorize', 401, false)
    ).toBe(true);
    expect(
      shouldPreserveOAuthConsentUnauthorized(
        '/oauth/authorize?state=test',
        401,
        false
      )
    ).toBe(true);
    expect(
      shouldPreserveOAuthConsentUnauthorized(
        'https://api.gitroom.com/oauth/authorize?state=test',
        401,
        false
      )
    ).toBe(true);
    expect(
      shouldPreserveOAuthConsentUnauthorized('/user/profile', 401, false)
    ).toBe(false);
    expect(
      shouldPreserveOAuthConsentUnauthorized('/oauth/authorize', 401, true)
    ).toBe(false);
  });

@JOY
JOY (JOY) merged commit 0df3a5d into dev Aug 31, 2026
10 checks passed
@JOY
JOY (JOY) deleted the codex/beta-oauth-callback-fix branch August 31, 2026 14:57
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