Skip to content

OCPBUGS-71237: Persist console sessions across pod restarts - #16911

Open
jhadvig wants to merge 8 commits into
openshift:mainfrom
jhadvig:OCPBUGS-71237
Open

OCPBUGS-71237: Persist console sessions across pod restarts#16911
jhadvig wants to merge 8 commits into
openshift:mainfrom
jhadvig:OCPBUGS-71237

Conversation

@jhadvig

@jhadvig jhadvig commented Aug 5, 2026

Copy link
Copy Markdown
Member

Analysis / Root cause:

Console sessions are stored entirely in per-pod process memory. When a pod restarts (upgrades, scaling, OOM, operator reconciliation), all sessions are lost and users must re-authenticate. This affects all users but is most disruptive for clusters using external IdPs (htpasswd, LDAP) where re-authentication requires full credential entry.

Two architectural limitations cause this:

  1. Cookie encryption keys are generated randomly per process — cookies from one pod can't be decrypted by another
  2. The refresh token cookie stores only a reference ID pointing to an in-memory map, not the actual token

Jira: https://redhat.atlassian.net/browse/OCPBUGS-71237
Related: https://redhat.atlassian.net/browse/OCPBUGS-58468
Companion operator PR: openshift/console-operator#1204

Solution description:

Phase 1: Shared encryption keys

Accept file-based encryption keys (provided by the console-operator via session-secret Secret) instead of generating random keys per process. Falls back to random generation when no key files are provided (backward compat with older operators).

Phase 2: Cookie-based session recovery

For OIDC auth (refresh tokens available):

  • Store the actual encrypted refresh token in the cookie instead of a reference ID
  • Existing recovery mechanism exchanges it with the OIDC provider for new tokens
  • Backward-compatible fallback for old-format cookies

For OpenShift auth (no refresh tokens — OAuth server doesn't support refresh_token grant):

  • New openshift-recovery-token cookie stores encrypted access token + expiry
  • recoverSession() reads the token, validates expiry locally, creates a new server-side session
  • No OAuth server interaction needed — fully transparent backend recovery

Security hardening

  • Refresh token value removed from error log messages
  • Refresh token revoked at OAuth server on logout
  • Cookie MaxLength raised to 8192 for large OIDC JWT refresh tokens
  • All type assertions use comma-ok guards
  • Dead refreshTokenID field removed

Screenshots / screen recording:

Test setup:
Deploy cluster with htpasswd IdP, login, restart console pods, verify session persists.

Test cases:

  • kubeadmin session persists across pod restart (verified on live cluster)
  • htpasswd IDP user session persists across pod restart (verified on live cluster)
  • Logout clears session, recovery cookie, and revokes tokens
  • Old-format cookies still work via fallback
  • E2e test: session survives pod deletion and plugin toggle

Browser conformance:

  • Chrome
  • Firefox
  • Safari (or Epiphany on Linux)

Additional info:

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added session recovery using secure recovery cookies.
    • Improved session persistence across console restarts and component rollouts.
    • Added support for optional OpenShift cookie authentication keys.
    • Logout now revokes refresh tokens when possible and completes cleanup even if revocation fails.
  • Bug Fixes

    • Preserved compatibility with existing session cookies.
    • Prevented sensitive refresh-token values from appearing in error messages.

Store the actual encrypted OAuth refresh token in the browser cookie
instead of a reference ID that maps to an in-memory store. This allows
any console pod to recover a user's session after a restart by decrypting
the cookie and exchanging the refresh token with the OAuth server.

To enable cross-pod cookie decryption, accept shared encryption keys
from files (managed by the console-operator via a session-secret Secret)
instead of generating random keys per process. The existing recovery
mechanism in getLoginState() already handles the token refresh — the
only gap was making the refresh token available from the cookie.

Backward compatible: old-format cookies with reference IDs are still
accepted via fallback lookup during rolling upgrades.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Aug 5, 2026
@openshift-ci-robot

Copy link
Copy Markdown
Contributor

@jhadvig: This pull request references Jira Issue OCPBUGS-71237, which is invalid:

  • expected the bug to target the "5.0.0" version, but no target version was set

Comment /jira refresh to re-evaluate validity if changes to the Jira bug are made, or edit the title of this pull request to link to a different bug.

The bug has been updated to refer to the pull request using the external bug tracker.

Details

In response to this:

Analysis / Root cause:

Console sessions are stored entirely in per-pod process memory. When a pod restarts (upgrades, scaling, OOM, operator reconciliation), all sessions are lost and users must re-login. The refresh token cookie only stores a 32-char reference ID that maps to the actual token in an in-memory map — after restart, the map is empty and the reference resolves to nothing.

Jira: https://redhat.atlassian.net/browse/OCPBUGS-71237
Related: https://redhat.atlassian.net/browse/OCPBUGS-58468, https://redhat.atlassian.net/browse/OCPBUGS-82365

Solution description:

Two coordinated changes (this PR + console-operator PR):

1. Shared encryption keys (console-operator side)
The operator now generates a session-secret Secret with persistent encryption keys for all auth types (previously OIDC-only) and mounts them into console pods.

2. Cookie-based session recovery (this PR)

  • Accept external encryption keys from files instead of generating random keys per process (auth_openshift.go)
  • Store the actual encrypted refresh token in the cookie instead of a reference ID (combined_sessions.go)
  • The existing recovery mechanism in getLoginState() already handles token refresh — it just needed the actual token from the cookie
  • Backward compatible: old-format cookies with reference IDs are still accepted via fallback lookup during rolling upgrades
  • Removed dead refreshTokenID field and accessor from LoginState
  • Added comma-ok guards on all cookie value type assertions

Recovery flow after pod restart:

  1. Pod restarts → in-memory SessionStore is empty
  2. Browser sends request with encrypted refresh token cookie
  3. GetSession() → nil (empty maps)
  4. getLoginState() calls GetCookieRefreshToken() which decrypts actual token from cookie
  5. refreshSession() exchanges token with OAuth server → new access token
  6. New server-side session created, user continues without login prompt

Screenshots / screen recording:

Test setup:

  • Deploy cluster with htpasswd or LDAP IdP (kubeadmin masks the issue)
  • Login to console, restart console pods, verify session persists

Test cases:

  • Session persists across single pod restart
  • Session persists across rolling upgrade (all pods replaced)
  • Old-format cookies (pre-upgrade) still work on the same pod
  • Logout properly clears session and cookie
  • Multiple concurrent users recover sessions after restart
  • Cookie size stays within 4096 byte limit

Browser conformance:

  • Chrome
  • Firefox
  • Safari (or Epiphany on Linux)

Additional info:

  • Requires coordinated console-operator PR for session-secret Secret generation
  • Based on Jefferson Ramos' solution exploration (Approach A: Cookie-Based Session Recovery)
  • Pattern follows oauth2-proxy and Kubernetes Dashboard precedents

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci-robot openshift-ci-robot added the jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. label Aug 5, 2026
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 5, 2026
@openshift-ci

openshift-ci Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

OpenShift authentication now accepts paired cookie keys. Session cookies store actual refresh tokens and retain legacy lookup. Recovery cookies restore missing sessions. Playwright tests verify console session persistence after pod recovery and plugin rollout.

Changes

OpenShift session persistence

Layer / File(s) Summary
Cookie configuration and authentication wiring
cmd/bridge/config/session/sessionoptions.go, pkg/auth/oauth2/auth.go, pkg/auth/oauth2/auth_oidc.go, pkg/auth/oauth2/auth_openshift.go, pkg/auth/sessions/server_session.go
OpenShift validates paired cookie keys, passes configured keys into authentication, and reuses or generates both keys.
Direct refresh-token session storage
pkg/auth/sessions/loginstate.go, pkg/auth/sessions/server_session.go, pkg/auth/sessions/combined_sessions.go, pkg/auth/oauth2/auth_oidc_test.go, pkg/auth/sessions/combined_sessions_test.go
Session cookies store refresh tokens directly. Session operations support legacy ID lookup, token rotation, stale-token indexing, and mapping cleanup.
Recovery and logout handling
pkg/auth/oauth2/auth_openshift.go, pkg/auth/sessions/combined_sessions.go, pkg/auth/sessions/combined_sessions_test.go
Login sets recovery cookies, missing sessions can be recreated from valid recovery cookies, and logout revokes refresh tokens before cleanup.
Console session persistence coverage
frontend/e2e/tests/console/session-persistence.spec.ts
Playwright tests verify authentication after console pod recovery and ConsolePlugin rollout, then restore plugin configuration.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Playwright
  participant Console
  participant OpenShiftAuthenticator
  participant CombinedSessionStore
  participant ConsoleOperator
  participant ConsoleDeployment
  Playwright->>Console: authenticate and open dashboard
  Console->>OpenShiftAuthenticator: request login state
  OpenShiftAuthenticator->>CombinedSessionStore: read session or recovery cookie
  CombinedSessionStore-->>OpenShiftAuthenticator: return authenticated state
  Playwright->>ConsoleDeployment: delete console pods
  ConsoleDeployment-->>Console: recover deployment
  Playwright->>Console: verify existing session
  Playwright->>ConsoleOperator: disable enabled ConsolePlugin
  ConsoleOperator->>ConsoleDeployment: trigger rollout
  Playwright->>Console: verify existing session after rollout
Loading

Suggested reviewers: rhamilto, leo6leo

🚥 Pre-merge checks | ✅ 14 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the Jira issue and the primary change: persisting console sessions across pod restarts.
Description check ✅ Passed The description covers the root cause, solution, testing, browser conformance, dependencies, and related Jira issues.
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.
Stable And Deterministic Test Names ✅ Passed The PR adds no Ginkgo tests or dynamic Ginkgo titles; its new Playwright titles are static and descriptive.
Test Structure And Quality ✅ Passed The PR adds standard Go tests and a Playwright spec; no changed test uses Ginkgo or Ginkgo lifecycle/assertion APIs, so this Ginkgo-specific check is not applicable.
Microshift Test Compatibility ✅ Passed The added e2e test is a Playwright TypeScript test using test.describe/test, not a Ginkgo test; this MicroShift check is therefore not applicable.
Single Node Openshift (Sno) Test Compatibility ✅ Passed The PR adds Playwright tests, not Ginkgo e2e tests. They delete console pods and wait for readiness without requiring distinct nodes, HA, or multi-node topology.
Topology-Aware Scheduling Compatibility ✅ Passed The PR changes auth/session code and an e2e test only; diff searches found no manifests, controllers, or topology, affinity, selector, toleration, replica, or PDB constraints.
Ote Binary Stdout Contract ✅ Passed The PR adds no OTE binary or Ginkgo suite setup. Touched Go files have no process-level entrypoints, and added output is handler-level klog only; klog v2 defaults to stderr.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed The added e2e coverage uses Playwright test.describe, not Ginkgo, and uses the configured console URL plus Kubernetes APIs without hardcoded IPv4 addresses or public network endpoints.
No-Weak-Crypto ✅ Passed The PR adds no MD5, SHA-1, DES, RC4, Blowfish, or ECB code; cookies use existing securecookie AES-CTR/HMAC-SHA-256 with constant-time MAC verification.
Container-Privileges ✅ Passed The PR changes no container or Kubernetes manifests and adds no prohibited privilege settings; the e2e test only deletes pods and patches ConsolePlugin configuration.
No-Sensitive-Data-In-Logs ✅ Passed Added production logs do not format access or refresh tokens; the refresh error now omits the token, and recovery logging uses generic messages.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@openshift-ci openshift-ci Bot added the component/backend Related to backend label Aug 5, 2026

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

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 `@pkg/auth/sessions/combined_sessions_test.go`:
- Around line 288-296: Update the legacy-cookie test in
combined_sessions_test.go so it actually exercises legacy resolution through
GetSession: keep the existing byRefreshTokenID setup, add a LoginState entry to
byRefreshToken for "refresh-old", and change the assertion to expect that
session state instead of nil. Use the GetSession path and the existing
testCookies/refresh-old symbols to verify that a legacy cookie resolves
correctly during rolling upgrade.
🪄 Autofix

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: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: efcb9ee2-7afd-4bed-a0f5-e37fcad5a946

📥 Commits

Reviewing files that changed from the base of the PR and between e5715aa and 8c83365.

📒 Files selected for processing (9)
  • cmd/bridge/config/session/sessionoptions.go
  • pkg/auth/oauth2/auth.go
  • pkg/auth/oauth2/auth_oidc.go
  • pkg/auth/oauth2/auth_oidc_test.go
  • pkg/auth/oauth2/auth_openshift.go
  • pkg/auth/sessions/combined_sessions.go
  • pkg/auth/sessions/combined_sessions_test.go
  • pkg/auth/sessions/loginstate.go
  • pkg/auth/sessions/server_session.go
💤 Files with no reviewable changes (1)
  • pkg/auth/sessions/server_session.go

Comment thread pkg/auth/sessions/combined_sessions_test.go
@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Aug 5, 2026
- Remove refresh token value from error log message to prevent
  credential leakage in pod logs
- Revoke refresh token (OAuthAuthorizeToken) at the OAuth server
  on logout to prevent cookie replay attacks
- Raise securecookie MaxLength to 8192 to accommodate large OIDC
  JWT refresh tokens that exceed the default 4096 limit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/auth/oauth2/auth_openshift.go (1)

75-94: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate configured cookie key lengths before constructing the session store. OpenShift validation checks that both keys are set or both are unset, but it does not validate file contents. Require an authentication key of at least 32 bytes and an encryption key of 16, 24, or 32 bytes; otherwise NewCookieStore records the cipher error and cookie operations fail.

🤖 Prompt for 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.

In `@pkg/auth/oauth2/auth_openshift.go` around lines 75 - 94, Before constructing
the session store in the key-selection flow, validate configured cookie key
lengths: require cookieAuthenticationKey to be at least 32 bytes and
cookieEncryptionKey to be exactly 16, 24, or 32 bytes. Return a clear validation
error for invalid configured values, while preserving random key generation when
both keys are unset.
🤖 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 `@pkg/auth/oauth2/auth_openshift.go`:
- Around line 216-221: Update the logout handler around
OAuthAuthorizeTokens().Delete so a refresh-token revocation error is returned as
a failure response instead of only being logged; stop the flow before deleting
the local session or returning 204. Preserve the existing successful revocation
behavior, and add a test covering the failed deletion path.
- Around line 216-221: Update logout’s access-token revocation failure path to
clear the session before returning: preserve the revocation error separately,
perform refresh-token cleanup and call sessions.DeleteSession, then return the
original error. Ensure cleanup also runs when OAuthAccessTokens().Delete fails
rather than exiting before the existing logout cleanup flow.

---

Outside diff comments:
In `@pkg/auth/oauth2/auth_openshift.go`:
- Around line 75-94: Before constructing the session store in the key-selection
flow, validate configured cookie key lengths: require cookieAuthenticationKey to
be at least 32 bytes and cookieEncryptionKey to be exactly 16, 24, or 32 bytes.
Return a clear validation error for invalid configured values, while preserving
random key generation when both keys are unset.
🪄 Autofix

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: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ace1f7b9-a182-4ba0-a71a-e452c7615b95

📥 Commits

Reviewing files that changed from the base of the PR and between 8c83365 and 9d72995.

📒 Files selected for processing (2)
  • pkg/auth/oauth2/auth_openshift.go
  • pkg/auth/sessions/combined_sessions.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/auth/sessions/combined_sessions.go

Comment on lines +216 to +221
if refreshToken := ls.RefreshToken(); refreshToken != "" {
if delErr := oauthClient.OAuthAuthorizeTokens().Delete(ctx, tokenToObjectName(refreshToken), metav1.DeleteOptions{}); delErr != nil {
klog.V(4).Infof("failed to revoke refresh token on logout: %v", delErr)
}
}

@coderabbitai coderabbitai Bot Aug 5, 2026

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.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- relevant symbols ---'
rg -n -C 12 'func .*DeleteSession|DeleteSession\(|failed to revoke refresh token|OAuthAuthorizeTokens\(\)\.Delete|RefreshToken\(\)' pkg/auth/oauth2 pkg/auth/sessions
printf '%s\n' '--- bounded source slice ---'
sed -n '180,245p' pkg/auth/oauth2/auth_openshift.go

Repository: openshift/console

Length of output: 35701


Broken Authentication (CWE-613): Insufficient Session Expiration

Reachability: External · Exploitability: Moderate

Return an error when refresh-token revocation fails.

OAuthAuthorizeTokens().Delete errors are logged, but the handler still deletes the local session and returns 204. A copied refresh-token cookie can remain valid. Return a failure response and add a test for this path.

🤖 Prompt for 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.

In `@pkg/auth/oauth2/auth_openshift.go` around lines 216 - 221, Update the logout
handler around OAuthAuthorizeTokens().Delete so a refresh-token revocation error
is returned as a failure response instead of only being logged; stop the flow
before deleting the local session or returning 204. Preserve the existing
successful revocation behavior, and add a test covering the failed deletion
path.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '180,235p' pkg/auth/oauth2/auth_openshift.go
printf '\n-- related symbols --\n'
rg -n -C 3 'DeleteSession|refreshSession|OAuthAccessTokens\(\)\.Delete|OAuthAuthorizeTokens\(\)\.Delete|RefreshToken\(\)' pkg/auth/oauth2 pkg/auth/sessions

Repository: openshift/console

Length of output: 20240


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

p = Path("pkg/auth/oauth2/auth_openshift.go")
lines = p.read_text().splitlines()
window = "\n".join(lines[177:224])
required = [
    "err = oauthClient.OAuthAccessTokens().Delete",
    'http.Error(w, "removing the session failed", http.StatusInternalServerError)',
    "return",
    'if refreshToken := ls.RefreshToken(); refreshToken != "" {',
    "o.sessions.DeleteSession(w, r)",
]
positions = {text: window.find(text) for text in required}
print("logout control-flow positions:", positions)
print("access-delete failure returns before refresh cleanup:",
      positions[required[1]] < positions[required[3]] and positions[required[2]] < positions[required[3]])
print("access-delete failure returns before local session deletion:",
      positions[required[1]] < positions[required[4]] and positions[required[2]] < positions[required[4]])
PY

printf '\n-- session cookie deletion and refresh lookup --\n'
sed -n '88,108p' pkg/auth/sessions/combined_sessions.go
sed -n '219,250p' pkg/auth/sessions/combined_sessions.go
sed -n '252,274p' pkg/auth/oauth2/auth_openshift.go

Repository: openshift/console

Length of output: 3274


Broken Authentication (CWE-613): Insufficient Session Expiration

Reachability: External

Clear the session when access-token revocation fails.

When OAuthAccessTokens().Delete returns an error, logout returns before refresh-token cleanup and o.sessions.DeleteSession. The refresh-token cookie and server-side session can then reach refreshSession. Run cleanup on this failure path and preserve the revocation error separately.

🤖 Prompt for 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.

In `@pkg/auth/oauth2/auth_openshift.go` around lines 216 - 221, Update logout’s
access-token revocation failure path to clear the session before returning:
preserve the revocation error separately, perform refresh-token cleanup and call
sessions.DeleteSession, then return the original error. Ensure cleanup also runs
when OAuthAccessTokens().Delete fails rather than exiting before the existing
logout cleanup flow.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These seem like valid concerns, have you looked into them yet?

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- auth OpenShift structure and logout-related tests ---'
ast-grep outline pkg/auth/oauth2/auth_openshift.go --items all --type function --match 'logout|DeleteSession|getLoginState|refreshSession'
ast-grep outline pkg/auth/oauth2/auth_openshift_test.go --items all --type function --match 'logout|Logout|DeleteSession|refresh'

printf '%s\n' '--- session store structure ---'
ast-grep outline pkg/auth/sessions/combined_sessions.go --items all --type function --match 'DeleteSession|GetSession|GetCookieRefreshToken|ClearRecoveryCookie'

printf '%s\n' '--- logout and deletion references with context ---'
rg -n -C 8 -P '\b(?:logout|DeleteSession|OAuthAccessTokens\(\)\.Delete|OAuthAuthorizeTokens\(\)\.Delete|GetCookieRefreshToken)\s*\(' \
  pkg/auth/oauth2 pkg/auth/sessions \
  -g '*.go'

printf '%s\n' '--- bounded implementation slices ---'
sed -n '180,245p' pkg/auth/oauth2/auth_openshift.go
sed -n '200,300p' pkg/auth/sessions/combined_sessions.go

Length of output: 31672


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate OpenShift authentication test files ---'
fd -t f -e go . pkg/auth/oauth2 | sort

printf '%s\n' '--- OpenShift auth construction and logout test references ---'
rg -n -C 10 -P '\b(?:newOpenShiftAuth|openShiftAuth|LogoutFunc|func\s+Test.*(?:OpenShift|Logout)|OAuthAuthorizeTokens|OAuthAccessTokens)\b' \
  pkg/auth/oauth2 -g '*_test.go'

printf '%s\n' '--- test helpers that provide HTTP/API-client behavior ---'
rg -n -C 8 -P '\b(?:httptest\.NewServer|RoundTripper|Transport:|http\.Client|NewForConfig|issuerURL|testValidRefreshToken)\b' \
  pkg/auth/oauth2 -g '*_test.go'

printf '%s\n' '--- complete local deletion and refresh-token lookup behavior ---'
sed -n '120,175p' pkg/auth/sessions/combined_sessions.go
sed -n '220,270p' pkg/auth/sessions/combined_sessions.go

Length of output: 1403


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- auth test file structure ---'
ast-grep outline pkg/auth/oauth2/auth_test.go --items all --type function
printf '%s\n' '--- bounded auth test source ---'
sed -n '1,260p' pkg/auth/oauth2/auth_test.go

printf '%s\n' '--- repository-wide OpenShift logout and API client test references ---'
rg -n -C 8 -P '\b(?:newOpenShiftAuth|openShiftAuth|OAuthAuthorizeTokens|OAuthAccessTokens|LogoutFunc|logout)\b' \
  -g '*_test.go' . || true

printf '%s\n' '--- HTTP transport fakes in authentication tests ---'
rg -n -C 8 -P '\b(?:RoundTripper|Transport|httptest\.NewServer|http\.Client|NewForConfig)\b' \
  pkg/auth -g '*_test.go' || true

printf '%s\n' '--- relevant auth construction ---'
sed -n '240,340p' pkg/auth/oauth2/auth.go

Length of output: 37782


@TheRealJon, yes. I reviewed the implementation, the session-store behavior, and the existing tests.

Both findings identify valid defects:

  1. If OAuthAccessTokens().Delete fails, logout returns before it calls DeleteSession and ClearRecoveryCookie. The encrypted refresh-token cookie remains available. getLoginState can use it to refresh or restore the session.

  2. If OAuthAuthorizeTokens().Delete fails, the remote refresh token can remain valid. The current handler logs the error and returns 204.

The first finding needs one correction. On a refresh-token revocation failure, the handler must not return before local cleanup. It must expire the local session, refresh-token cookie, and recovery cookie first. It should then return an error response to report that remote revocation failed.

The robust flow is:

  • Attempt both remote revocations.
  • Record any revocation error.
  • Always clear local session state and cookies.
  • Return 204 only if both remote revocations succeed.
  • Return 500 after local cleanup if either remote revocation fails.

There is no existing OpenShift logout test. pkg/auth/oauth2/auth_test.go only covers authenticator construction. The change should add failure-path tests that assert both the error response and local cookie/session cleanup.

You are interacting with an AI system.

Two test cases:
- Session survives console pod deletion and replacement
- Session survives plugin toggle (operator-triggered rollout)

Both tests log in (kubeadmin by default, htpasswd when env vars
are set), trigger a console pod restart, and assert the user is
still authenticated without a login redirect.

Tagged @slow — pod rollout takes 30-60s per test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@openshift-ci

openshift-ci Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: jhadvig

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
frontend/e2e/tests/console/session-persistence.spec.ts (1)

33-33: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use Playwright baseURL-resolved routes.

Full URLs bypass Playwright route resolution and treat WEB_CONSOLE_URL as a route prefix. Use leading-slash routes.

  • frontend/e2e/tests/console/session-persistence.spec.ts#L33-L33: use page.goto('/dashboards').
  • frontend/e2e/tests/console/session-persistence.spec.ts#L55-L58: use page.goto('/k8s/cluster/nodes').
  • frontend/e2e/tests/console/session-persistence.spec.ts#L128-L131: use page.goto('/dashboards').

Based on learnings: use page.goto('/k8s/...') with a leading-slash absolute path in console E2E specs.

🤖 Prompt for 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.

In `@frontend/e2e/tests/console/session-persistence.spec.ts` at line 33, Update
all three page.goto calls in
frontend/e2e/tests/console/session-persistence.spec.ts at lines 33-33, 55-58,
and 128-131 to use leading-slash Playwright routes: /dashboards,
/k8s/cluster/nodes, and /dashboards respectively, removing baseURL
interpolation.

Source: Learnings

🤖 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 `@frontend/e2e/tests/console/session-persistence.spec.ts`:
- Line 14: Update the baseURL initialization in the session persistence test to
remove the page.url() fallback; use WEB_CONSOLE_URL when set, otherwise default
directly to http://localhost:9000 before passing it to performLogin.
- Around line 50-52: Update the console session-persistence test in
frontend/e2e/tests/console/session-persistence.spec.ts at lines 50-52 and
120-124: before deleting console pods, record their UIDs and require Ready
replacement pods with different UIDs; before applying the plugin patch, record
metadata.generation and wait for it to increase before calling
waitForDeploymentReady. Ensure readiness checks cannot succeed against stale
pods or an unchanged deployment.

---

Nitpick comments:
In `@frontend/e2e/tests/console/session-persistence.spec.ts`:
- Line 33: Update all three page.goto calls in
frontend/e2e/tests/console/session-persistence.spec.ts at lines 33-33, 55-58,
and 128-131 to use leading-slash Playwright routes: /dashboards,
/k8s/cluster/nodes, and /dashboards respectively, removing baseURL
interpolation.
🪄 Autofix

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: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b35579f3-c0f2-4253-b387-ebc49baf1e8a

📥 Commits

Reviewing files that changed from the base of the PR and between 9d72995 and e200791.

📒 Files selected for processing (1)
  • frontend/e2e/tests/console/session-persistence.spec.ts

Comment thread frontend/e2e/tests/console/session-persistence.spec.ts Outdated
Comment on lines +50 to +52
await test.step('Wait for new console pods to be ready', async () => {
await k8sClient.waitForDeploymentReady(CONSOLE_DEPLOYMENT, CONSOLE_NAMESPACE, 180_000);
});

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the readiness helper and determine whether it checks a target generation
# or newly created pods rather than only current Deployment readiness.
rg -n -C 8 'waitForDeploymentReady|waitFor.*Pod|metadata\.generation|updatedReplicas' frontend/e2e

Repository: openshift/console

Length of output: 15116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- session-persistence.spec.ts ---'
sed -n '1,145p' frontend/e2e/tests/console/session-persistence.spec.ts

printf '%s\n' '--- waitForDeploymentReady and related client methods ---'
sed -n '580,750p' frontend/e2e/clients/kubernetes-client.ts

printf '%s\n' '--- console pod discovery and deployment patch call sites ---'
rg -n -C 12 'CONSOLE_DEPLOYMENT|CONSOLE_NAMESPACE|list.*Pod|readNamespacedPod|patchNamespacedCustomObject|plugins' frontend/e2e/tests/console/session-persistence.spec.ts frontend/e2e/clients/kubernetes-client.ts

Repository: openshift/console

Length of output: 30341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from dataclasses import dataclass

`@dataclass`
class DeploymentObservation:
    desired: int
    available: int
    updated: int
    available_condition: bool
    generation: int
    pod_uids: tuple[str, ...]

def wait_for_deployment_ready(d: DeploymentObservation) -> bool:
    return (
        d.available == d.desired
        and d.updated == d.desired
        and d.available_condition
    )

old = DeploymentObservation(2, 2, 2, True, 7, ("old-a", "old-b"))

# A stale observation can pass immediately after deletion is accepted.
stale_after_delete = DeploymentObservation(2, 2, 2, True, 7, ("old-a", "old-b"))
assert wait_for_deployment_ready(stale_after_delete)

# A plugin change can leave the Deployment ready without a new generation.
no_rollout_after_plugin_patch = DeploymentObservation(2, 2, 2, True, 7, ("old-a", "old-b"))
assert wait_for_deployment_ready(no_rollout_after_plugin_patch)
assert no_rollout_after_plugin_patch.generation == old.generation

# The intended checks distinguish both cases.
replacement = DeploymentObservation(2, 2, 2, True, 7, ("new-a", "new-b"))
assert not set(replacement.pod_uids) & set(old.pod_uids)

rollout = DeploymentObservation(2, 2, 2, True, 8, ("new-a", "new-b"))
assert rollout.generation > old.generation

print("waitForDeploymentReady ignores generation and pod UIDs")
print("stale readiness and no-rollout readiness both satisfy its predicate")
PY

Repository: openshift/console

Length of output: 278


Gate readiness on the requested disruption.

  • Before deleting console pods, record their UIDs and wait for Ready replacement pods with different UIDs.
  • Before the plugin patch, record metadata.generation and wait for it to increase before calling waitForDeploymentReady. Otherwise, stale readiness can pass without a rollout.
📍 Affects 1 file
  • frontend/e2e/tests/console/session-persistence.spec.ts#L50-L52 (this comment)
  • frontend/e2e/tests/console/session-persistence.spec.ts#L120-L124
🤖 Prompt for 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.

In `@frontend/e2e/tests/console/session-persistence.spec.ts` around lines 50 - 52,
Update the console session-persistence test in
frontend/e2e/tests/console/session-persistence.spec.ts at lines 50-52 and
120-124: before deleting console pods, record their UIDs and require Ready
replacement pods with different UIDs; before applying the plugin patch, record
metadata.generation and wait for it to increase before calling
waitForDeploymentReady. Ensure readiness checks cannot succeed against stale
pods or an unchanged deployment.

OpenShift's internal OAuth server does not support refresh tokens,
so the refresh-token-in-cookie approach only works for OIDC auth.

For OpenShift auth, store the encrypted access token + expiry in a
separate recovery cookie (openshift-recovery-token). After pod restart,
the backend reads the token from the cookie, validates expiry locally,
and creates a new server-side session transparently. No OAuth server
interaction or page redirect needed.

- SetRecoveryCookie/GetRecoveryCookie/ClearRecoveryCookie methods
- recoverSession() called in getLoginState() when session is nil
- Recovery cookie cleared on logout and DeleteSession
- MaxAge set to match token expiry
- Unit tests for recovery cookie lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

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

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 `@pkg/auth/sessions/combined_sessions_test.go`:
- Around line 310-356: Update the recovery-cookie tests around the request
constructions in the relevant subtests to capture the error returned by each
http.NewRequest call and immediately assert it with require.NoError(t, err).
Apply this to every affected request, including req and req2 in the set/get,
empty-request, clear-cookie, and expired-token cases, without changing the test
behavior.

In `@pkg/auth/sessions/combined_sessions.go`:
- Around line 263-285: Handle errors returned by CookieStore.Get in both
SetRecoveryCookie and GetRecoveryCookie. In SetRecoveryCookie, return a wrapped
decode error or initialize a fresh session before mutating and saving; in
GetRecoveryCookie, return the existing empty values with false when decoding
fails, before accessing session values.
🪄 Autofix

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: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f56d416b-5576-4e6b-8775-88ccab4263b1

📥 Commits

Reviewing files that changed from the base of the PR and between e200791 and 8afcc78.

📒 Files selected for processing (4)
  • pkg/auth/oauth2/auth_openshift.go
  • pkg/auth/sessions/combined_sessions.go
  • pkg/auth/sessions/combined_sessions_test.go
  • pkg/auth/sessions/server_session.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/auth/sessions/server_session.go

Comment thread pkg/auth/sessions/combined_sessions_test.go Outdated
Comment on lines +263 to +285
func (cs *CombinedSessionStore) SetRecoveryCookie(w http.ResponseWriter, r *http.Request, accessToken string, expiry time.Time) error {
s, _ := cs.clientStore.Get(r, openshiftRecoveryTokenCookieName)
s.Values["access-token"] = accessToken
s.Values["expiry"] = expiry.Unix()
maxAge := int(time.Until(expiry).Seconds())
if maxAge > 0 {
s.Options.MaxAge = maxAge
}
return s.Save(r, w)
}

func (cs *CombinedSessionStore) GetRecoveryCookie(r *http.Request) (string, time.Time, bool) {
s, _ := cs.clientStore.Get(r, openshiftRecoveryTokenCookieName)
accessToken, ok := s.Values["access-token"].(string)
if !ok || accessToken == "" {
return "", time.Time{}, false
}
expiryUnix, ok := s.Values["expiry"].(int64)
if !ok {
return "", time.Time{}, false
}
return accessToken, time.Unix(expiryUnix, 0), true
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For github.com/gorilla/sessions v1.4.0, what does CookieStore.Get return when a cookie cannot be decoded or verified?

💡 Result:

For gorilla/sessions v1.4.0, when a cookie cannot be decoded or verified, CookieStore.Get returns a new session object along with a non-nil error [1][2]. Specifically: - The session object returned is a fresh session (i.e., its IsNew field will be true) [1][2]. - The error returned indicates the reason the existing cookie could not be successfully decoded or verified (such as a signature mismatch or expired/invalid data) [3][4]. Because Get always returns a session, even if it cannot decode the provided cookie, it is common practice in many applications to ignore the error returned by Get if you simply intend to proceed with a blank or new session when the existing one is invalid [5][6]. Calling session.Save on this returned session will effectively overwrite the invalid cookie with a new, valid one in the user's browser [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file ---'
sed -n '230,310p' pkg/auth/sessions/combined_sessions.go

printf '%s\n' '--- call sites ---'
rg -n -C 3 'GetRecoveryCookie|SetRecoveryCookie' .

printf '%s\n' '--- dependency declarations ---'
rg -n -C 2 'gorilla/sessions|sessions v' go.mod go.sum vendor 2>/dev/null || true

printf '%s\n' '--- CookieStore.Get implementation references ---'
rg -n -C 8 'func \(.*CookieStore.*\) Get|type CookieStore|Decode' $(go env GOPATH 2>/dev/null)/pkg/mod/github.com/gorilla/sessions* 2>/dev/null || true

Repository: openshift/console

Length of output: 21836


🌐 Web query:

github.com/gorilla/sessions v1.4.0 CookieStore.Get source store.go securecookie.Decode error behavior

💡 Result:

In github.com/gorilla/sessions v1.4.0, the CookieStore.Get method behaves as a convenience wrapper around Registry.Get [1], which in turn calls CookieStore.New if a session does not already exist in the registry [1][2]. When CookieStore.New encounters an issue decoding an existing cookie (e.g., due to invalid keys, tampered data, or expired timestamps handled by the securecookie package), it returns a new, empty session along with the error [1]. Key aspects of this behavior include: 1. Always Returns a Session: CookieStore.Get (via New) is designed to always return a *Session object, even if an error occurs during the decoding process [1][3]. This ensures that application logic can proceed with a fresh session if the previous one is unreadable [4]. 2. Error Indication: The error returned indicates that while a cookie was present, it could not be successfully decoded using the configured codecs [1]. 3. Recommended Handling: The official gorilla/sessions documentation and community guidance suggest that while the error can be checked, Get() often returns a valid (though empty or new) session object regardless of the error [3][5]. Developers often choose to ignore the error returned by Get() if they intend to treat a failed decryption as a session reset (i.e., treating the user as unauthenticated) [5]. 4. Underlying Mechanism: The securecookie.Decode method (called within CookieStore.New) performs several validation steps, including checking the length, verifying the MAC (message authentication code), validating timestamps (if configured), and decrypting the payload [6]. Any failure in these steps results in an error [6], which CookieStore bubbles up [1]. Common scenarios where securecookie.Decode returns an error include key rotation mismatches, modified or malformed cookie values, and expired sessions [4][6][7]. Since CookieStore continues by returning a new *Session with IsNew set to true, the application effectively invalidates the unreadable session and starts a new one [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- session store construction and related error handling ---'
rg -n -C 5 'clientStore|NewCookieStore|CookieStore|CombinedSessionStore' pkg/auth/sessions pkg/auth/oauth2 | head -240

printf '%s\n' '--- recovery-cookie tests ---'
sed -n '280,375p' pkg/auth/sessions/combined_sessions_test.go

printf '%s\n' '--- vendor registry behavior ---'
sed -n '70,125p' vendor/github.com/gorilla/sessions/store.go
sed -n '1,115p' vendor/github.com/gorilla/sessions/registry.go 2>/dev/null || true

printf '%s\n' '--- standalone source-level behavioral verifier ---'
python3 - <<'PY'
from pathlib import Path
p = Path("vendor/github.com/gorilla/sessions/store.go")
text = p.read_text()
start = text.index("func (s *CookieStore) New")
end = text.index("\n}", start) + 2
snippet = text[start:end]
assert "return session, err" in snippet
assert "session.IsNew = true" in snippet
assert "if err == nil" in snippet and "session.IsNew = false" in snippet
print("CookieStore.New returns the allocated session with the decode error and keeps IsNew=true on decode failure.")
print("CookieStore.Get delegates to the registry, which returns that result.")
PY

Repository: openshift/console

Length of output: 22748


Handle recovery-cookie decode errors.

CookieStore.Get returns a new session and a non-nil error when decoding fails. Handle this error at both call sites. Return a wrapped error from SetRecoveryCookie, or explicitly create a fresh session before saving. Return false from GetRecoveryCookie when decoding fails.

🤖 Prompt for 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.

In `@pkg/auth/sessions/combined_sessions.go` around lines 263 - 285, Handle errors
returned by CookieStore.Get in both SetRecoveryCookie and GetRecoveryCookie. In
SetRecoveryCookie, return a wrapped decode error or initialize a fresh session
before mutating and saving; in GetRecoveryCookie, return the existing empty
values with false when decoding fails, before accessing session values.

Source: Path instructions

@jhadvig jhadvig changed the title WIP OCPBUGS-71237: Persist console sessions across pod restarts OCPBUGS-71237: Persist console sessions across pod restarts Aug 10, 2026
@jhadvig
jhadvig marked this pull request as ready for review August 10, 2026 11:36
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 10, 2026
@openshift-ci
openshift-ci Bot requested review from Leo6Leo and rhamilto August 10, 2026 11:37
jhadvig and others added 2 commits August 10, 2026 15:06
- gofmt formatting on 4 Go files
- Replace page.waitForTimeout with pod deletion in e2e test
  to satisfy playwright/no-wait-for-timeout ESLint rule

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
During initial cluster install, the console config may reference
session key file paths before the session-secret Secret is created
and mounted. For OpenShift auth, treat missing key files as non-fatal
(log warning, fall back to random keys) instead of crashing the
console process. OIDC auth retains the fatal error since key files
are required for that mode.

This fixes the bootstrap timing issue where the console deployment
fails with ProgressDeadlineExceeded because pods crash before the
operator creates the session-secret.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jhadvig

jhadvig commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Test Results

All scenarios verified on a live cluster (GCP) with both console and console-operator PRs deployed together.

# Test User Trigger Result
1 Session persists after pod restart kubeadmin oc delete pods -l component=ui ✅ PASSED
2 Session persists after pod restart testuser (htpasswd IDP) oc delete pods -l component=ui ✅ PASSED
3 Session persists after plugin disable kubeadmin Operator-triggered rollout ✅ PASSED
4 Session persists after plugin re-enable kubeadmin Operator-triggered rollout ✅ PASSED
5 Session persists after plugin disable testuser (htpasswd IDP) Operator-triggered rollout ✅ PASSED
6 Session persists after plugin re-enable testuser (htpasswd IDP) Operator-triggered rollout ✅ PASSED
7 Cluster installs successfully Fresh install via cluster-bot ✅ PASSED

Test methodology

Companion operator PR: openshift/console-operator#1204

/verified by @jhadvig

@openshift-ci-robot openshift-ci-robot added the verified Signifies that the PR passed pre-merge verification criteria label Aug 11, 2026
@openshift-ci-robot

Copy link
Copy Markdown
Contributor

@jhadvig: This PR has been marked as verified by @jhadvig.

Details

In response to this:

Test Results

All scenarios verified on a live cluster (GCP) with both console and console-operator PRs deployed together.

# Test User Trigger Result
1 Session persists after pod restart kubeadmin oc delete pods -l component=ui ✅ PASSED
2 Session persists after pod restart testuser (htpasswd IDP) oc delete pods -l component=ui ✅ PASSED
3 Session persists after plugin disable kubeadmin Operator-triggered rollout ✅ PASSED
4 Session persists after plugin re-enable kubeadmin Operator-triggered rollout ✅ PASSED
5 Session persists after plugin disable testuser (htpasswd IDP) Operator-triggered rollout ✅ PASSED
6 Session persists after plugin re-enable testuser (htpasswd IDP) Operator-triggered rollout ✅ PASSED
7 Cluster installs successfully Fresh install via cluster-bot ✅ PASSED

Test methodology

Companion operator PR: openshift/console-operator#1204

/verified by @jhadvig

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@TheRealJon TheRealJon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just following up on the two major coderabbit concerns. Other than that, lgtm.

Comment on lines +216 to +221
if refreshToken := ls.RefreshToken(); refreshToken != "" {
if delErr := oauthClient.OAuthAuthorizeTokens().Delete(ctx, tokenToObjectName(refreshToken), metav1.DeleteOptions{}); delErr != nil {
klog.V(4).Infof("failed to revoke refresh token on logout: %v", delErr)
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These seem like valid concerns, have you looked into them yet?

- Move local session cleanup to defer in logout() so cookies are
  always cleared even when remote token revocation fails (CWE-613)
- Remote revocations are now best-effort; logout always returns 204
- Add positive test case for legacy cookie resolution via byRefreshToken
- Remove page.url() fallback from e2e test baseURL (returns about:blank)
- Add require.NoError to http.NewRequest calls in recovery cookie tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@openshift-ci-robot openshift-ci-robot removed the verified Signifies that the PR passed pre-merge verification criteria label Aug 12, 2026
@jhadvig

jhadvig commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

@TheRealJon addressed both CodeRabbit concerns:

Concern 1 (access token revocation failure leaves cookies behind) — Valid, fixed. Moved local cleanup (DeleteSession + ClearRecoveryCookie) to a defer at the top of logout() so it always runs before any return or WriteHeader. Remote revocations are now best-effort (logged at error level, non-fatal). Logout always returns 204. This also fixes a subtle issue where http.Error() calls WriteHeader internally, causing Go's ResponseWriter to silently drop any subsequent Set-Cookie headers.

Concern 2 (refresh token revocation failure) — Not a real issue for OpenShift auth. The internal OAuth server doesn't support refresh_token grant type (grant_types_supported only lists authorization_code and implicit), so ls.RefreshToken() is always empty and the OAuthAuthorizeTokens().Delete() code path never executes. For OIDC auth, auth_oidc.go has its own logout handler. The existing best-effort handling (log at V(4), continue) is correct for the theoretical case.

Also addressed the three minor nits (legacy cookie positive test, page.url() fallback removal, require.NoError on test requests).

Accept optional previous key pair files for cookie decryption.
gorilla/securecookie tries each key pair in order — new cookies
use the current keys, old cookies are still readable with the
previous keys until they expire.

- NewSessionStore accepts variadic previousKeyPairs
- SessionOptions reads previous key file paths from config
- Previous keys are always optional and non-fatal if missing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@shahsahil264

Copy link
Copy Markdown
Contributor

/test backend

@shahsahil264

Copy link
Copy Markdown
Contributor

/retest

@shahsahil264

Copy link
Copy Markdown
Contributor

/test backend

1 similar comment
@shahsahil264

Copy link
Copy Markdown
Contributor

/test backend

@openshift-ci

openshift-ci Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@jhadvig: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/backend 60c662a link true /test backend

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. component/backend Related to backend jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants