Skip to content

fix(dashboard): unbreak CORS on expired session for k8s API calls - #2788

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/dashboard-cors-and-api-route
Jul 14, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/dashboard-cors-and-api-route

Conversation

@IvanHunters

@IvanHunters IvanHunters commented Jun 2, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Fix two related issues that cause the dashboard SPA to break with CORS errors once the kc-access cookie expires.

  1. KeycloakClient for dashboard had no webOrigins set, so the operator did not touch the field in Keycloak after initial creation. After any rootUrl change the existing webOrigins value got stuck at an old hostname and blocked CORS from the SPA. Setting it to + makes Keycloak derive the allowed origins from redirectUris.

  2. oauth2-proxy answered 302 (redirect to Keycloak /auth) for unauthenticated XHR/fetch on /api and /apis. The login page does not emit Access-Control-Allow-Origin, so the browser blocked the fetch with a CORS error and the SPA had no way to recover. Adding --api-route makes oauth2-proxy answer 401 without a redirect for these paths, so the SPA can handle re-auth itself via a top-level navigation.

Release note

fix(dashboard): dashboard SPA no longer fails with CORS errors after the kc-access cookie expires; oauth2-proxy returns 401 (not 302) for /api and /apis, and the KeycloakClient declares webOrigins: ["+"].

Summary by CodeRabbit

Chores

  • Updated API routing configuration to properly handle requests to API endpoints
  • Enhanced web origins configuration for improved authentication system compatibility

… paths

Two related issues caused the dashboard SPA to break with CORS errors
after the kc-access cookie expired:

1. KeycloakClient for dashboard had no webOrigins set, so the operator
   never touched the field in Keycloak after initial creation. After
   any rootUrl change the existing webOrigins value became stale and
   blocked CORS from the SPA. Setting "+" tells Keycloak to derive
   allowed origins from redirectUris.

2. oauth2-proxy returned 302 -> Keycloak /auth for unauthenticated
   XHR/fetch on /api and /apis. The login page does not emit
   Access-Control-Allow-Origin, so the SPA's fetch failed with CORS.
   --api-route makes oauth2-proxy answer 401 without redirect, so the
   SPA can handle the re-auth itself via top-level navigation.

Signed-off-by: Ivan Okhotnikov <[email protected]>
@coderabbitai

coderabbitai Bot commented Jun 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aa62cb9e-de87-4785-951e-6789769adbff

📥 Commits

Reviewing files that changed from the base of the PR and between f0f1431 and be8f6b4.

📒 Files selected for processing (2)
  • packages/system/dashboard/templates/gatekeeper.yaml
  • packages/system/dashboard/templates/keycloakclient.yaml

📝 Walkthrough

Walkthrough

This PR updates dashboard authentication configuration across two Kubernetes templates. The Gatekeeper deployment adds API routing rules to forward requests under /api and /apis paths through the OIDC auth-proxy. The KeycloakClient spec explicitly sets web origins with a permissive pattern to allow OIDC callbacks from those API endpoints.

Changes

Dashboard Authentication Configuration

Layer / File(s) Summary
Auth-proxy routing and OIDC origin configuration
packages/system/dashboard/templates/gatekeeper.yaml, packages/system/dashboard/templates/keycloakclient.yaml
Gatekeeper auth-proxy adds two --api-route argument patterns to route /api and /apis requests. KeycloakClient spec adds webOrigins field set to "+" for OIDC origin allowlisting.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

A rabbit hops through auth so bright,
Routes and origins set just right,
/api and /apis now flow,
With webOrigins all aglow! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(dashboard): unbreak CORS on expired session for k8s API calls' directly matches the PR's primary objective of fixing CORS failures in the dashboard after session expiration. It's concise, specific, and clearly summarizes the main change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dashboard-cors-and-api-route

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 and usage tips.

@github-actions github-actions Bot added area/dashboard Issues or PRs related to the dashboard / UI kind/bug Categorizes issue or PR as related to a bug labels Jun 2, 2026
@dosubot dosubot Bot added the kind/backport Categorizes issue or PR as requiring a backport to the current release line label Jun 2, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses CORS-related failures in the dashboard SPA that occur when the session cookie expires. By adjusting the Keycloak client configuration and modifying how the proxy handles unauthenticated API requests, the system now correctly signals the need for re-authentication to the frontend without triggering browser-level CORS blocks.

Highlights

  • CORS Configuration: Updated the KeycloakClient to include webOrigins: ['+'], allowing Keycloak to dynamically derive allowed origins from redirectUris and preventing stale hostname issues.
  • API Authentication Handling: Configured oauth2-proxy with --api-route for /api and /apis paths to return 401 Unauthorized instead of a 302 redirect, enabling the SPA to handle session expiration gracefully.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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 configures additional API routes (/api and /apis) for the gatekeeper template and adds wildcard web origins (+) to the Keycloak client template. The reviewer suggests also adding /openapi and /version to the gatekeeper's --api-route list to prevent potential CORS or redirect issues on session expiration.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +71 to +72
- --api-route=^/api(/|$)
- --api-route=^/apis(/|$)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In addition to /api and /apis, Kubernetes frontend applications and SPAs often query other root-level API endpoints such as /version (to retrieve cluster version info) and /openapi (to fetch OpenAPI schemas for resource validation). To prevent potential CORS/302 redirect issues on these endpoints when the session expires, it is recommended to add them to the --api-route list as well.

            - --api-route=^/api(/|$)
            - --api-route=^/apis(/|$)
            - --api-route=^/openapi(/|$)
            - --api-route=^/version(/|$)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — the CORS fix is correct: marking /api and /apis as oauth2-proxy api-routes returns 401 (not a cross-origin 302) on an expired session, and webOrigins: "+" scopes CORS headers to the client's redirect-URI origins. Two non-blocking coverage gaps.

Business context: when a dashboard session expires, k8s API calls (XHR to /api / /apis) hit oauth2-proxy's default 302-to-keycloak, which the browser blocks as a cross-origin redirect — so the SPA breaks instead of re-authenticating. This returns 401 for those paths and lets keycloak emit CORS headers.

Verified:

  • --api-route=^/api(/|$) / ^/apis(/|$): oauth2-proxy returns 401 instead of a 302 for matched paths on auth failure, so the SPA's fetch sees a clean 401 rather than a blocked cross-origin redirect to keycloak. Correct.
  • webOrigins: ["+"]: the KeycloakClient CRD documents + as "permit all origins of Valid Redirect URIs" (explicitly not the * wildcard). The dashboard client's redirectUris resolve to origin https://dashboard.<host>, so + scopes CORS to exactly that origin (plus any extra-keycloak-redirect-uri-for-dashboard). Consistent with the existing post.logout.redirect.uris: "+" on the same client — safe and idiomatic.

Non-blocking follow-ups

  1. Coverage of other root paths. The fix covers /api and /apis. oauth2-proxy fronts the whole cozy-dashboard-console upstream, so if the SPA also calls /version or /openapi cross-path on an expired session, those still 302 → the same CORS break (raised by the bot review). The SPA source isn't in this repo, so I can't confirm it calls them — worth either extending --api-route to ^/version(/|$) and ^/openapi(/|$), or confirming the console never serves those to the SPA on an expired session.
  2. No test assertion for the new args. tests/gatekeeper_test.yaml pins the auth-proxy container's probes/ports but not its args, so the new --api-route flags aren't covered. Given that file's own stated goal ("pin both branches so structural drift is a test failure"), an assertion that the OIDC-enabled branch carries --api-route=^/api(/|$) and ^/apis(/|$) would guard against a future refactor silently dropping them and reintroducing the CORS break.

myasnikovdaniil added a commit that referenced this pull request Jun 29, 2026
…lently

When the kc-access cookie expires, k8s API calls return 401 and the SPA was left stuck on a stale page until the user manually cleared the cache. K8sClient now treats 401 as an auth boundary: a one-shot top-level navigation to /oauth2/start?rd=<current-url> (the oauth2-proxy re-auth convention). Overridable via K8sClientConfig.onUnauthorized; no-op outside the browser. A unauthorizedHandled latch prevents repeated redirects on concurrent 401s.

Re-applied from cozystack/cozystack-ui#34 onto the in-tree console after the UI was vendored (#2963); the source repo is being archived. Related: #2788. A unit regression test is a follow-up — the k8s-client workspace package has no test harness yet.

Co-authored-by: IvanHunters <[email protected]>
Assisted-By: Claude <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
@lexfrei
Aleksei Sviridkin (lexfrei) merged commit f478915 into main Jul 14, 2026
12 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the fix/dashboard-cors-and-api-route branch July 14, 2026 10:07
@github-actions

Copy link
Copy Markdown

Successfully created backport PR for release-1.5:

myasnikovdaniil added a commit that referenced this pull request Jul 15, 2026
…n for k8s API calls (#3291)

# Description
Backport of #2788 to `release-1.5`.
myasnikovdaniil added a commit that referenced this pull request Aug 15, 2026
…lently

When the kc-access cookie expires, k8s API calls return 401 and the SPA was left stuck on a stale page until the user manually cleared the cache. K8sClient now treats 401 as an auth boundary: a one-shot top-level navigation to /oauth2/start?rd=<current-url> (the oauth2-proxy re-auth convention). Overridable via K8sClientConfig.onUnauthorized; no-op outside the browser. A unauthorizedHandled latch prevents repeated redirects on concurrent 401s.

Re-applied from cozystack/cozystack-ui#34 onto the in-tree console after the UI was vendored (#2963); the source repo is being archived. Related: #2788. A unit regression test is a follow-up — the k8s-client workspace package has no test harness yet.

Co-authored-by: IvanHunters <[email protected]>
Assisted-By: Claude <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
Aleksei Sviridkin (lexfrei) pushed a commit that referenced this pull request Oct 7, 2026
…lently

When the kc-access cookie expires, k8s API calls return 401 and the SPA was left stuck on a stale page until the user manually cleared the cache. K8sClient now treats 401 as an auth boundary: a one-shot top-level navigation to /oauth2/start?rd=<current-url> (the oauth2-proxy re-auth convention). Overridable via K8sClientConfig.onUnauthorized; no-op outside the browser. A unauthorizedHandled latch prevents repeated redirects on concurrent 401s.

Re-applied from cozystack/cozystack-ui#34 onto the in-tree console after the UI was vendored (#2963); the source repo is being archived. Related: #2788. A unit regression test is a follow-up — the k8s-client workspace package has no test harness yet.

Co-authored-by: IvanHunters <[email protected]>
Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
Aleksei Sviridkin (lexfrei) pushed a commit that referenced this pull request Oct 7, 2026
…lently

When the kc-access cookie expires, k8s API calls return 401 and the
SPA stays on a stale page until the user clears the cache by hand.
K8sClient now treats 401 as an auth boundary and makes one top-level
navigation into the oauth2-proxy sign-in flow, passing the current
location as rd so the user comes back to the same page. The handler
can be replaced through K8sClientConfig.onUnauthorized and does
nothing outside the browser. The unauthorizedHandled latch keeps
concurrent 401s from redirecting more than once.

Re-applied from cozystack/cozystack-ui#34 onto the in-tree console
after the UI was vendored (#2963), since the source repo is being
archived. Related: #2788.

Co-authored-by: IvanHunters <[email protected]>
Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Oct 7, 2026
…3115)

## What this PR does

When the dashboard session expires, the console now sends the user back
to sign-in instead of sitting on a stale page. This re-applies
cozystack/cozystack-ui#34 by @IvanHunters onto the in-tree console,
since the UI was vendored into the monorepo (#2963) and the standalone
repo is being archived. It is part of the cozystack-ui retirement
tracked in #3097.

When the `kc-access` cookie expires, k8s API calls return 401.
`K8sClient` now treats a 401 as an auth boundary and makes one top-level
navigation to `/oauth2/sign_in?rd=<current-url>`. That path is the
sign-in entry both gatekeeper modes serve. With OIDC enabled,
oauth2-proxy goes straight to the provider (`--skip-provider-button`)
and returns the user to `rd`. Without OIDC, token-proxy renders its
token form there, but it does not honour `rd` yet, so the user lands on
`/` after sign-in.

The handler can be replaced through `K8sClientConfig.onUnauthorized` and
does nothing outside the browser. A latch keeps concurrent 401s from
redirecting more than once, and any 2xx from either fetch site re-arms
it, so a handler that re-authenticates in place still sees the next
expiry.

Related: #2788.

### Verification

`client.unauthorized.test.ts` covers the hook on the request and watch
paths, the latch across concurrent 401s, the re-arm after a success, and
the default handler's target and relative `rd`. `pnpm typecheck` and the
console test suite pass.

Original PR: cozystack/cozystack-ui#34, credited via `Co-authored-by`.

### Screenshots

Not a UI change.

### Downstream repositories

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

### Release note

```release-note
fix(dashboard): on a 401 (expired session) the console now sends the user to sign in again instead of silently showing a stale page. With OIDC enabled the user returns to the page they were on; without OIDC they land on the start page after entering a token.
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/dashboard Issues or PRs related to the dashboard / UI kind/backport Categorizes issue or PR as requiring a backport to the current release line kind/bug Categorizes issue or PR as related to a bug size/XS This PR changes 0-9 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants