Skip to content

fix: revoke OpenShift OAuth token server-side and prevent re-login loop on logout#709

Open
jkilzi wants to merge 7 commits into
mainfrom
fix/openshift-logout
Open

fix: revoke OpenShift OAuth token server-side and prevent re-login loop on logout#709
jkilzi wants to merge 7 commits into
mainfrom
fix/openshift-logout

Conversation

@jkilzi

@jkilzi jkilzi commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Fixes #708

Summary

  • OpenShiftAuthHandler.Logout was a no-op since PR EDM-2612: Logout method in OCP is POST. Remove cookie on logout #393 (EDM-2612), clearing only the FlightCtl session cookie but leaving the OpenShift OAuth session alive
  • With a single non-K8s provider configured, LoginPage immediately re-initiated the OAuth flow → users were silently re-authenticated after clicking Logout
  • Redirecting the browser to {oauth-server}/logout (the approach attempted in the previous revision of this PR) returned HTTP 405 Method Not Allowed for unauthenticated GET requests

Fix — proxy (Go)

OpenShiftAuthHandler.Logout now revokes the access token server-side:

DELETE /apis/oauth.openshift.io/v1/oauthaccesstokens/{name}
Authorization: Bearer <token>

The token resource name is derived from the raw token value: sha256~-prefixed tokens are hashed with SHA-256 and base64url-encoded; legacy tokens are used as-is. The proxy always returns an empty redirect URL — no browser redirect needed.

Fix — frontend (TypeScript)

To prevent the silent re-login loop after the session is cleared:

  • logout() in apiCalls.ts sets a sessionStorage flag (flightctl.justLoggedOut) before navigating to /login
  • LoginPage reads and clears the flag; when set, it skips the single-provider auto-redirect (suppressAutoSelect) and displays the provider selector so the user must click explicitly to log back in

Test plan

  • TestOpenShiftTokenName_SHA256Prefix — correct SHA-256 + base64url derivation for sha256~ tokens
  • TestOpenShiftTokenName_LegacyToken — legacy token returned unchanged
  • TestOpenShiftLogout_RevokesTokenViaDelete — DELETE is issued to the correct URL with Bearer auth; 200/204 treated as success
  • TestOpenShift_LogoutRevocationLogged_OnUnexpectedStatus — unexpected status is logged as a warning, not an error
  • TestOpenShiftLogout_NoOpWhenTokenEmpty — no HTTP call when token is empty
  • TestOpenShiftLogout_NoOpWhenAPIServerURLEmpty — no HTTP call when apiServerURL is unset
  • Manual: click Logout on a cluster using the OpenShift OAuth provider; browser lands on /login showing the provider selector without auto-redirecting

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jkilzi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 66b0aa79-88a5-4e2b-ac52-6344a602ab10

📥 Commits

Reviewing files that changed from the base of the PR and between 6257b75 and 19af6f4.

📒 Files selected for processing (3)
  • apps/standalone/src/app/components/Login/LoginPage.tsx
  • proxy/auth/openshift.go
  • proxy/auth/openshift_test.go

Walkthrough

OpenShift logout now revokes the OAuth access token server-side, and the standalone UI records a logout flag so the login page does not immediately reselect a single provider after redirecting to /login. Tests cover token-name derivation, revocation requests, and redirect helper updates.

Changes

OpenShift logout revocation

Layer / File(s) Summary
Test harness and redirect helper
proxy/auth/helpers_test.go, proxy/auth/redirect_test.go
TestMain initializes auth-test logging, and redirectBaseMatchesRequest now accepts t while keeping the same origin comparison behavior. Three redirect tests were updated to pass t.
OpenShift token revocation
proxy/auth/openshift.go, proxy/auth/openshift_test.go
Logout now derives an OAuth access-token name, sends an authenticated DELETE to the OpenShift revocation endpoint, and still returns ("", nil) after attempting revocation. Tests cover token-name hashing and logout request behavior, including empty-input and failure cases.

Standalone logout redirect state

Layer / File(s) Summary
Logout redirect flag and login suppression
apps/standalone/src/app/utils/apiCalls.ts, apps/standalone/src/app/components/Login/LoginPage.tsx
Logout stores a shared session-storage flag before navigating to /login, and the login page clears that flag and suppresses single-provider auto-selection when it is present.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

proxy, standalone

Poem

A logout clicked, a token fell,
The OAuth gate said, “fare thee well.”
A little flag in session stayed,
So login did not auto-play.
The user stays logged out today ✨


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 4 warnings)

Check name Status Explanation Resolution
No-Sensitive-Data-In-Logs ❌ Error Added logout error logs use WithError(err); transport/request errors can embed the revoke URL, leaking token names and hostnames into logs. Stop logging raw errors here; log a redacted/generic message or strip URL/token details before emitting the failure.
Ai-Attribution ❌ Error The PR’s feature commit includes Co-authored-by: Cursor <cursoragent@cursor.com>, which this check forbids for AI tools; no accepted AI trailer is attached to that commit. Replace the AI co-author line with an accepted trailer such as Made-with: Cursor or Assisted-by:/Generated-by: in the commit message, and do not use Co-authored-by for AI tools.
Linked Issues check ⚠️ Warning The PR revokes access tokens and suppresses auto-redirect, but it does not implement the required authURL-based /logout flow for OpenShift session termination. Switch Logout to build the OpenShift logout URL from authURL, issue the browser redirect there, and keep regression tests for valid and invalid authURL inputs.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Unchecked-Errors ⚠️ Warning proxy/auth/openshift.go defers resp.Body.Close() at line 180, silently discarding a returned error with no justification comment. Handle or log the Close() error (e.g. check it in a defer), or add an explicit comment if intentionally ignored.
I18n-Compliance ⚠️ Warning LoginPage.tsx has a user-visible Alert title set to plain "Error" instead of t(), and no non-literal t() keys were found. Wrap the alert title in t('Error') or an existing translation key so the login error UI is localized.
✅ Passed checks (9 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed All changes are directly tied to logout handling, login-page suppression, or regression tests.
No-Hardcoded-Secrets ✅ Passed No hardcoded secrets, credentialed URLs, or long base64 blobs were added; only synthetic token literals appear in tests.
No-Weak-Crypto ✅ Passed Touched code uses SHA-256 via the stdlib only; no MD5/SHA1/DES/RC4/3DES/Blowfish or custom secret comparisons were added.
No-Injection-Vectors ✅ Passed No eval/exec/dangerouslySetInnerHTML/yaml.load sinks were added in the touched Go/TS files; searches of the new code and tests found none.
Container-Privileges ✅ Passed No changed container/K8s manifests add privileged, hostPID/Network/IPC, SYS_ADMIN, root, or allowPrivilegeEscalation settings; touched YAMLs are OAuth/secret configs only.
Resource-Leaks ✅ Passed PASS: The new Go code closes resp.Body via defer, httptest.Server is closed in tests, and no goroutines or file handles were introduced.
Generated-Files-Not-Hand-Edited ✅ Passed Touched files are proxy/auth and apps/standalone sources; none are in the generated libs/types or i18n translation paths.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: server-side OpenShift token revocation and preventing logout re-login loops.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/openshift-logout

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@proxy/auth/helpers_test.go`:
- Around line 9-20: The test helper function redirectBaseMatchesRequest is
missing proper failure attribution. Add a *testing.T parameter as the first
argument to the redirectBaseMatchesRequest function signature, then call
t.Helper() at the beginning of the function body to ensure test failures are
correctly attributed to the caller rather than the helper itself. Update all
call sites of this function to pass the *testing.T value from the calling test.

In `@proxy/auth/openshift_test.go`:
- Around line 7-73: Consolidate the four separate Logout test functions
(TestOpenShiftLogout_ReturnsLogoutURLDerivedFromAuthURL,
TestOpenShiftLogout_StripsExistingPathAndQuery,
TestOpenShiftLogout_FallsBackGracefullyWhenAuthURLIsEmpty, and
TestOpenShiftLogout_FallsBackGracefullyWhenAuthURLIsInvalid) into a single
table-driven test. Create a test case struct containing fields for authURL,
token, redirectURL, expectedLogoutURL, and expectedError, then define a slice of
test cases capturing all four scenarios. Replace the individual test functions
with a single table-driven test that loops through cases using t.Run(), calling
the OpenShiftAuthHandler Logout method for each case and validating the returned
logout URL and error against expected values.

In `@proxy/auth/openshift.go`:
- Around line 141-145: The logout URL construction in the openshift.go file does
not validate the Scheme component of the parsed URL, which can result in a
malformed URL like "://.../logout" when parsing URLs with empty schemes such as
"//oauth.example.com/x". In the validation condition where you check for parsing
errors and empty Host (around line 141-142), add an additional check to ensure
parsed.Scheme is not empty. If the scheme is empty, the function should return
the graceful fallback of an empty string and nil, just like it does for parsing
errors or missing host values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 63794846-233e-4f07-9cd9-f66f5d7fcb43

📥 Commits

Reviewing files that changed from the base of the PR and between 66e5e62 and bada853.

📒 Files selected for processing (3)
  • proxy/auth/helpers_test.go
  • proxy/auth/openshift.go
  • proxy/auth/openshift_test.go

Comment thread proxy/auth/helpers_test.go
Comment thread proxy/auth/openshift_test.go Outdated
Comment thread proxy/auth/openshift.go Outdated
@jkilzi

jkilzi commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Regarding the Unchecked-Errors warning: the url.Parse error on line 141 is checked — when it returns a non-nil error the function falls back to ("", nil). This is intentional: a logout URL derivation failure should degrade gracefully (no logout redirect, session cookie is still cleared) rather than surface an error to the caller, which would block the logout flow. There is nothing actionable the caller can do with a parse error here, so returning it would only add noise.

jkilzi added a commit that referenced this pull request Jun 23, 2026
- Add parsed.Scheme guard to prevent malformed logout URL for scheme-less
  inputs like "//host/path" (url.Parse returns Host but empty Scheme)
- Use url.URL struct to build logout URL instead of string concatenation
- Add *testing.T + t.Helper() to redirectBaseMatchesRequest; log only
  scheme://host in error message to avoid leaking query parameters
- Consolidate four separate Logout test functions into a single
  table-driven TestOpenShiftLogout per project conventions
Assisted-by: Cursor <cursoragent@cursor.com>

Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@proxy/auth/openshift_test.go`:
- Around line 25-34: The test suite for the OpenShift authentication handler is
missing a regression test case for scheme-less auth URLs in the format
`//host/path`. These URLs represent an edge case where URL parsing may populate
the Host field while leaving Scheme empty, which needs explicit coverage. Add a
new test case to the existing test table (alongside the "empty auth URL falls
back" and "invalid auth URL falls back" cases) with a name field describing the
scheme-less scenario, an authURL field containing a scheme-less URL like
`//example.com/path`, and a wantURL field set to empty string to verify it falls
back correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 5ed145fe-7579-42a1-9ec8-607add991b58

📥 Commits

Reviewing files that changed from the base of the PR and between bada853 and 4eec374.

📒 Files selected for processing (4)
  • proxy/auth/helpers_test.go
  • proxy/auth/openshift.go
  • proxy/auth/openshift_test.go
  • proxy/auth/redirect_test.go

Comment thread proxy/auth/openshift_test.go
jkilzi added a commit that referenced this pull request Jun 23, 2026
- Add parsed.Scheme guard to prevent malformed logout URL for scheme-less
  inputs like "//host/path" (url.Parse returns Host but empty Scheme)
- Use url.URL struct to build logout URL instead of string concatenation
- Add *testing.T + t.Helper() to redirectBaseMatchesRequest; log only
  scheme://host in error message to avoid leaking query parameters
- Consolidate four separate Logout test functions into a single
  table-driven TestOpenShiftLogout per project conventions
Assisted-by: Cursor <cursoragent@cursor.com>

Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com>
@jkilzi jkilzi force-pushed the fix/openshift-logout branch from 4eec374 to a152e3a Compare June 23, 2026 13:13
jkilzi and others added 2 commits June 23, 2026 16:18
OpenShiftAuthHandler.Logout was returning ("", nil) since PR #393
(EDM-2612), which cleared only the FlightCtl session cookie but left
the OpenShift OAuth browser session active. Because the login page
auto-redirects when a single non-K8s provider is configured, users
were silently re-authenticated immediately after clicking Logout.

The original discovery-based approach ({apiServerURL}/.well-known/
oauth-authorization-server) was problematic when apiServerURL pointed
to the K8s API server, producing https://api.cluster:6443/logout as
the logout target — an invalid endpoint.

Fix: derive the logout URL directly from authURL (the OAuth
authorization endpoint). authURL always holds the OAuth server host
regardless of how apiServerURL is configured, so taking scheme://host
and appending /logout gives the correct
https://oauth-openshift.apps.{cluster}/logout without any HTTP
round-trip.

Fixes #708

Assisted-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com>
- Add parsed.Scheme guard to prevent malformed logout URL for scheme-less
  inputs like "//host/path" (url.Parse returns Host but empty Scheme)
- Use url.URL struct to build logout URL instead of string concatenation
- Add *testing.T + t.Helper() to redirectBaseMatchesRequest; log only
  scheme://host in error message to avoid leaking query parameters
- Consolidate four separate Logout test functions into a single
  table-driven TestOpenShiftLogout per project conventions
- Add scheme-less auth URL regression test case to TestOpenShiftLogout
- Add doc comments to openshiftClientForRedirect, Logout, and
  GetLoginRedirectURL to improve docstring coverage
- Add comment explaining intentional url.Parse error swallow fallback
Assisted-by: Cursor <cursoragent@cursor.com>

Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@jkilzi jkilzi force-pushed the fix/openshift-logout branch from a152e3a to 9cdadf4 Compare June 23, 2026 13:29
jkilzi and others added 2 commits June 29, 2026 14:35
…logout

Replace the browser redirect to `{oauth-server}/logout` (which returned
HTTP 405 for unauthenticated GET requests) with a server-side token
revocation call:

  DELETE /apis/oauth.openshift.io/v1/oauthaccesstokens/{name}

The token name is derived from the raw access token via SHA-256 +
base64url encoding for `sha256~`-prefixed tokens; legacy tokens are used
as-is (the value is the resource name). The DELETE is authenticated with
the user's own Bearer token, so no extra service-account permission is
needed. The proxy always returns an empty redirect URL, leaving the
frontend to navigate to `/login`.

The frontend is also updated to prevent the silent re-login loop that
occurred when `LoginPage` detected a single non-K8s provider and
immediately re-initiated the OAuth flow (even though the FlightCtl
session had just been cleared):

- `logout()` in `apiCalls.ts` sets a `sessionStorage` flag
  (`flightctl.justLoggedOut`) before navigating to `/login`.
- `LoginPage` reads and clears the flag; when set, it skips the
  single-provider auto-redirect and shows the provider selector
  instead, so the user must click explicitly to log back in.

Unit tests for `openShiftTokenName` and the new `Logout` behaviour are
added in `openshift_test.go`; `helpers_test.go` gains a `TestMain` that
initialises the logger so tests can exercise the warning paths.

Closes #708

Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@jkilzi jkilzi changed the title fix: restore OpenShift OAuth session logout on user sign-out fix: revoke OpenShift OAuth token server-side and prevent re-login loop on logout Jun 29, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@proxy/auth/openshift.go`:
- Around line 160-162: Normalize the base URL used for token revocation in the
OpenShift auth flow so query strings and fragments don’t corrupt the request
path. In the revocation logic around openShiftTokenName and the revokeURL
construction, parse o.apiServerURL, clear RawQuery and Fragment, and rebuild the
base from Scheme and Host before appending the
/apis/oauth.openshift.io/v1/oauthaccesstokens/... path. This ensures the DELETE
in the logout/revoke path always targets the correct OAuthAccessToken endpoint.
- Around line 164-176: The OpenShift token revocation request in the auth flow
can block indefinitely because the local http.Client has no timeout. Update the
revocation path around the client.Do call in the revocation helper to use a
bounded timeout, either by setting an http.Client Timeout or by creating the
request with a context deadline, so the logout flow fails fast if the API server
stalls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 02b2b183-5eee-47c8-a16a-17f5da9b713e

📥 Commits

Reviewing files that changed from the base of the PR and between 4eec374 and 6257b75.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • apps/standalone/src/app/components/Login/LoginPage.tsx
  • apps/standalone/src/app/utils/apiCalls.ts
  • proxy/auth/helpers_test.go
  • proxy/auth/openshift.go
  • proxy/auth/openshift_test.go
  • proxy/auth/redirect_test.go

Comment thread proxy/auth/openshift.go Outdated
Comment thread proxy/auth/openshift.go
jkilzi and others added 3 commits June 29, 2026 14:47
ESLint sort-imports rule requires named imports within a declaration to be
sorted alphabetically. JUST_LOGGED_OUT_KEY (uppercase) sorts before apiProxy.

Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Line was over the print-width limit; Prettier wraps the ternary expression.

Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Two issues found by CodeRabbit on the Logout() implementation:

1. URL normalization: o.apiServerURL can carry a path, query string, or
   fragment when derived from AuthorizationUrl (e.g. .../oauth/authorize?x=y).
   Appending /apis/oauth.openshift.io/... to such a value would corrupt the
   DELETE path. Introduce openShiftAPIServerBase() to extract only
   scheme+host before constructing the revocation URL.

2. Missing timeout: the revocation http.Client had no Timeout, so a stalled
   API server would block the proxy handler indefinitely. Set a 10s timeout
   so logout degrades fast rather than hanging.

Tests added for openShiftAPIServerBase() and for the end-to-end behaviour
when apiServerURL contains path/query components.

Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@celdrake

Copy link
Copy Markdown
Collaborator

Thanks! I see for OCP (kubeadmin) it's behaving better now because it brings the user to the selection screen.
I will check this for other deployment methods and auth providers in a few weeks (after 1.3 code freeze).

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.

Logout does not work with OpenShift OAuth provider — user is immediately re-authenticated

2 participants