Skip to content

fix: return 404 instead of 401 for missing OAuth2 apps #18755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ThomasK33
Copy link
Member

Problem

Users were being automatically logged out when deleting OAuth2 applications.

Root Cause

  1. User deletes OAuth2 app successfully
  2. React Query automatically refetches the app data
  3. Management API incorrectly returned 401 Unauthorized for the missing app
  4. Frontend axios interceptor sees 401 and calls signOut()
  5. User gets logged out unexpectedly

Solution

  • Change management API to return 404 Not Found for missing OAuth2 apps
  • OAuth2 protocol endpoints continue returning 401 per RFC 6749
  • Rename writeInvalidClient to writeClientNotFound for clarity

Additional Changes

  • Add conditional OAuth2 navigation when experiment is enabled or in dev builds
  • Add isDevBuild() utility and buildInfo to dashboard context
  • Minor improvements to format script and warning dialogs

@ThomasK33 ThomasK33 requested a review from BrunoQuaresma July 4, 2025 13:33
@ThomasK33 ThomasK33 force-pushed the fix/oauth2-app-deletion-logout branch from 7fac49d to 932c04f Compare July 4, 2025 13:51
This prevents users from being logged out when deleting OAuth2 apps.
The frontend interceptor triggers logout on 401 responses, but React Query
refetches deleted apps and should get 404, not 401.

Also adds conditional OAuth2 navigation when experiment is enabled.

Change-Id: I48886144883539b7c51307f2a500f95be31dd383
Signed-off-by: Thomas Kosiewski <[email protected]>
@ThomasK33 ThomasK33 force-pushed the fix/oauth2-app-deletion-logout branch from 932c04f to eb425dd Compare July 4, 2025 13:52
@ThomasK33 ThomasK33 requested a review from johnstcn July 7, 2025 12:14
Comment on lines +26 to +30
_CANONICALIZE_CMD="python3"
elif command -v python >/dev/null 2>&1; then
_CANONICALIZE_CMD="python"
elif command -v perl >/dev/null 2>&1; then
_CANONICALIZE_CMD="perl"
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to run python or perl interpreters at all here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Those are fallbacks in case realpath or readlink are not available.

While working with CC hooks, I noticed that their file paths are wonky. Sometimes, they are relative, and sometimes, they are absolute, often a misformulation of both.

So this is just in case someone develops coder on an env that doesn’t have realpath or readlink.

Copy link
Member

Choose a reason for hiding this comment

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

So this is just in case someone develops coder on an env that doesn’t have realpath or readlink.

Can you give an example? I see these available in Nix and they're also available on darwin via Brew / coreutils.

Copy link
Member Author

Choose a reason for hiding this comment

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

windows

Copy link
Member Author

Choose a reason for hiding this comment

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

Unless we don't care about Windows development, then I'm happy to zap this

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, this is a CC hook, and CC only works on Linux/macOS.

I'll yeet the fallbacks later.

Copy link
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

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

Instead of deviating further from the OAuth2 spec, what do you think about instead modifying the interceptor to ignore this as a special case?

If the user's token has indeed expired, there will almost certainly be another 401 to trigger this logic and sign the user out.

Copy link
Collaborator

@BrunoQuaresma BrunoQuaresma left a comment

Choose a reason for hiding this comment

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

I left a few comments related to the FE code, but I don't think they are blockers. 👍

@@ -141,6 +141,7 @@ export const EditOAuth2AppPageView: FC<EditOAuth2AppProps> = ({
confirmLoading={mutatingResource.deleteApp}
name={app.name}
entity="OAuth2 application"
info="⚠️ Warning: Deleting this OAuth2 application will immediately invalidate all active sessions and API keys associated with it. Users currently authenticated through this application will be logged out and need to re-authenticate."
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this message is displayed inside of a DeleteDialog, which already has a style to warn users, we don't need to prefix it with "⚠️ Warning:".

@@ -554,6 +558,7 @@ describe("WorkspacePage", () => {
appearance: MockAppearanceConfig,
entitlements: MockEntitlements,
experiments: [],
buildInfo: { version: "v0.0.0-test" } as BuildInfoResponse,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: You can use a more complete build info object, from our existent mocks, to avoid casting partial values using as. The advantage on that, IMHO, is to have a more reliable value. Time to time, I face some bugs in tests because the value types are correct, but the value is missing some properties.

To "fix" that you could do:

import { MockBuildInfo } from "testHelpers/entities"

return <DashboardContext.Provider
          value={{
            buildInfo: { 
              ..MockBuildInfo,
              version: "v0.0.0-test"
            }
          }}
        />

@@ -56,6 +56,7 @@ export const withDashboardProvider = (
entitlements,
experiments,
appearance: MockAppearanceConfig,
buildInfo: { version: "v0.0.0-test" } as BuildInfoResponse,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as before. However, If having the version as v0.0.0-test is what we want to use by default, you could just use the MockBuildInfo directly, and update the value prop to use the new value.

Copy link
Member Author

Instead of deviating further from the OAuth2 spec, what do you think about instead modifying the interceptor to ignore this as a special case?

If the user's token has indeed expired, there will almost certainly be another 401 to trigger this logic and sign the user out.

We're not deviating from the RFC here.

These changes do not address the revocation endpoint (which will be part of a future PR), but a custom endpoint to delete OAuth2 apps in coder. Thus we're not constrained and free to return the codes we want or need.

@johnstcn
Copy link
Member

johnstcn commented Jul 7, 2025

We're not deviating from the RFC here.

These changes do not address the revocation endpoint (which will be part of a future PR), but a custom endpoint to delete OAuth2 apps in coder. Thus we're not constrained and free to return the codes we want or need.

Sorry, misread the scope. That sounds fine then.

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