Skip to content

frontend: warn user before token expiry and auto-logout on session end#5371

Open
prabindersinghh wants to merge 8 commits into
kubernetes-sigs:mainfrom
prabindersinghh:fix/token-expiry-5112
Open

frontend: warn user before token expiry and auto-logout on session end#5371
prabindersinghh wants to merge 8 commits into
kubernetes-sigs:mainfrom
prabindersinghh:fix/token-expiry-5112

Conversation

@prabindersinghh
Copy link
Copy Markdown
Contributor

@prabindersinghh prabindersinghh commented May 4, 2026

Summary

Adds a session expiry warning banner that counts down the last 2 minutes
of a token's lifetime and automatically logs the user out when the token
expires. The backend /me endpoint now exposes tokenExpiry so the
frontend can track session lifetime without reading httpOnly cookies.

Related Issue

Related #5112

Changes

  • Added tokenExpiry field to writeMeResponse in backend/pkg/auth/auth.go
  • Added TestHandleMe_IncludesTokenExpiry in backend/pkg/auth/auth_test.go
  • Added fetchClusterMe, ClusterMeResponse, and ClusterMeResult
    discriminated union in frontend/src/lib/auth.ts
  • Added new TokenExpiryNotification.tsx component — polls /me every
    60s, shows countdown warning banner when <2 min remain, auto-logouts
    on confirmed expiry
  • Mounted <TokenExpiryNotification /> in Layout.tsx alongside
    <AlertNotification />

Steps to Test

  1. Create a short-lived Kubernetes token:
    kubectl create token default --duration=180s
  2. Log in to Headlamp using that token
  3. Wait ~60 seconds — a yellow warning banner should appear with a
    countdown ("Session expires in 1:45")
  4. Wait for full expiry — banner turns red, user is auto-logged out
    and redirected to auth page with "Session expired. Logging out…"

Screenshots (if applicable)

Warning State (< 2 minutes remaining)

image

Expired State (auto logout)

image

Behavior with automatically refreshed tokens

For auth providers that silently refresh tokens, the experience is
transparent — the updated expiry is reflected on the next poll and the
banner either never appears or dismisses immediately. The countdown
resets on every successful poll.

Known limitation — OIDC users

For OIDC clusters, the backend middleware refreshes tokens reactively
within 10s of expiry. This creates a ~110 second window where the warning
banner may appear for OIDC users even though they will not actually be
logged out.

This can be addressed by detecting the auth type from the cluster config
on the frontend and suppressing the banner entirely when the provider is
known to support silent refresh. Happy to include this in the current PR
or keep it as a follow-up, whichever you prefer.

Notes for the Reviewer

  • Poll interval is currently 60s — open to making this configurable
    or tighter (30s) based on preference
  • PureTokenExpiryNotification accepts an injectable fetchClusterMeFn
    for unit testing
  • Currently handles only the active cluster in the URL — multi-cluster
    support left as a follow-up

@k8s-ci-robot k8s-ci-robot added the do-not-merge/invalid-commit-message Indicates that a PR should not merge because it has an invalid commit message. label May 4, 2026
@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla Bot commented May 4, 2026

CLA Signed
The committers listed above are authorized under a signed CLA.

@k8s-ci-robot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: prabindersinghh
Once this PR has been reviewed and has the lgtm label, please assign yolossn for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found 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

@k8s-ci-robot
Copy link
Copy Markdown
Contributor

Welcome @prabindersinghh!

It looks like this is your first PR to kubernetes-sigs/headlamp 🎉. Please refer to our pull request process documentation to help your PR have a smooth ride to approval.

You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. Here is the bot commands documentation.

You can also check if kubernetes-sigs/headlamp has its own contribution guidelines.

You may want to refer to our testing guide if you run into trouble with your tests not passing.

If you are having difficulty getting your pull request seen, please follow the recommended escalation practices. Also, for tips and tricks in the contribution process you may want to read the Kubernetes contributor cheat sheet. We want to make sure your contribution gets all the attention it needs!

Thank you, and welcome to Kubernetes. 😃

@k8s-ci-robot k8s-ci-robot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels May 4, 2026
@k8s-ci-robot k8s-ci-robot requested review from illume and vyncent-t May 4, 2026 17:50
@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. and removed cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels May 4, 2026
@prabindersinghh prabindersinghh force-pushed the fix/token-expiry-5112 branch from 6ae57b7 to ed52048 Compare May 4, 2026 18:00
@k8s-ci-robot k8s-ci-robot removed the do-not-merge/invalid-commit-message Indicates that a PR should not merge because it has an invalid commit message. label May 4, 2026
@skoeva skoeva requested a review from Copilot May 4, 2026 19:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds session-expiry awareness across the auth flow so the frontend can learn token lifetime from /clusters/:cluster/me, warn before expiry, and react when the session ends.

Changes:

  • Extended backend /me responses to include tokenExpiry derived from the JWT exp claim.
  • Added frontend fetchClusterMe types/helper for reading /clusters/:cluster/me and distinguishing 401 expiry from other failures.
  • Introduced TokenExpiryNotification in the main layout to poll session state, show an expiry banner, and trigger logout on confirmed expiry.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
frontend/src/lib/auth.ts Adds /me fetch helper and response/result types for token-expiry-aware frontend auth checks.
frontend/src/components/App/TokenExpiryNotification.tsx New banner/poller component for warning on impending expiry and logging out after expiry.
frontend/src/components/App/Layout.tsx Mounts the new token-expiry notification in the app shell.
backend/pkg/auth/auth.go Includes token expiry in /me responses after validating the token is still valid.
backend/pkg/auth/auth_test.go Adds backend test coverage for tokenExpiry being returned by /me.

Comment thread frontend/src/components/App/TokenExpiryNotification.tsx Outdated
Comment thread frontend/src/components/App/TokenExpiryNotification.tsx
Comment thread frontend/src/components/App/TokenExpiryNotification.tsx Outdated
Copy link
Copy Markdown
Contributor

@illume illume left a comment

Choose a reason for hiding this comment

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

Thanks for working on this.

Would you mind addressing the open Copilot review comments? Please mark each comment as resolved after addressing it.

@prabindersinghh
Copy link
Copy Markdown
Contributor Author

Thanks for the review! Working on addressing all three Copilot comments now, countdown timer, test coverage, and the logout delay gap. Will push shortly.

@prabindersinghh
Copy link
Copy Markdown
Contributor Author

Addressed all three points, added a per-second setInterval for the live countdown, fixed immediate local-expiry logout without waiting for the next /me poll, and added 4 Vitest tests covering the warning threshold, future token, tokenExpired logout, and route suppression. Resolved all Copilot conversations.

@illume illume requested a review from Copilot May 7, 2026 06:54
Copy link
Copy Markdown
Contributor

@illume illume left a comment

Choose a reason for hiding this comment

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

Thanks for these changes.

Can you please have a look at the git commits to see if they meet the contribution guidelines? We use a Linux kernel style of git commits. See the contributing guide for general context, and please see previous git commits with git log for examples.

Commits that need attention
  • frontend: fix countdown timer, add tests, fix immediate logout on expiry — Description must start with a capital letter — e.g. frontend: HomeButton: Fix the button not frontend: HomeButton: fix the button.
Commit guidelines
  • Use atomic commits focused on a single change.
  • Use the title format <area>: <Description of changes> — description must start with a capital letter.
  • Keep the title under 72 characters (soft requirement).
  • Explain the intention and why the change is needed.
  • Make commit titles meaningful and describe what changed.
  • Do not add code that a later commit rewrites; squash or reorder commits instead.
  • Do not include Fixes #NN in commit messages.

Good examples:

  • frontend: HomeButton: Fix so it navigates to home
  • backend: config: Add enable-dynamic-clusters flag

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread frontend/src/components/App/TokenExpiryNotification.tsx
Comment thread frontend/src/components/App/TokenExpiryNotification.test.tsx
Copy link
Copy Markdown
Contributor

@illume illume left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution.

There are some open Copilot review comments — could you take a look at them? Please mark each one as resolved once you've addressed it.

The GitHub CI test job has snapshot failures. Run cd frontend && npm run test -- -u to regenerate the snapshots.

How to update snapshots

Run cd frontend && npm run test -- -u to regenerate all snapshots. Review the diff to make sure the visual changes are intentional, then commit the updated snapshot files.

The frontend lint job in CI is failing. Run cd frontend && npm run lint to see the errors.

How to fix lint errors

Run cd frontend && npm run lint to see all ESLint errors. Many can be fixed automatically with cd frontend && npm run lint -- --fix. Remaining errors need manual attention.

The backend test job in CI is failing. Run cd backend && go test ./... to reproduce the errors locally.

How to run the backend tests

Run cd backend && go test ./... to see all failures. Fix the failing tests and commit the result.

@prabindersinghh prabindersinghh force-pushed the fix/token-expiry-5112 branch from 64c4ca1 to 62eca1c Compare May 7, 2026 22:15
@prabindersinghh
Copy link
Copy Markdown
Contributor Author

Thanks for the detailed feedback. Fixing the commit message casing, addressing the setInterval early-start issue, correcting the mock route paths, running lint, fixing backend tests, and regenerating snapshots. Will push a clean rebase shortly.

@prabindersinghh prabindersinghh force-pushed the fix/token-expiry-5112 branch from 62eca1c to 76794ca Compare May 7, 2026 22:18
@prabindersinghh
Copy link
Copy Markdown
Contributor Author

Pushed updated commits, fixed commit message casing to match headlamp style (capital after colon), addressed the setInterval early-start with a setTimeout delay, corrected mock route paths to match the real router, fixed lint (eqeqeq, import order), regenerated the pluginLib snapshot which now includes fetchClusterMe, and backend tests pass. Resolved all Copilot conversations.

@illume illume requested a review from Copilot May 18, 2026 07:12
Copy link
Copy Markdown
Contributor

@illume illume left a comment

Choose a reason for hiding this comment

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

Thanks for these changes.

Can you please have a look at the git commits to see if they meet the contribution guidelines? We use a Linux kernel style of git commits. See the contributing guide for general context, and please see previous git commits with git log for examples.

Commits that need attention
  • frontend: TokenExpiryNotification: key polling on cluster name not pathname — Description must start with a capital letter — e.g. frontend: HomeButton: Fix the button not frontend: HomeButton: fix the button.
Commit guidelines
  • Use atomic commits focused on a single change.
  • Use the title format <area>: <Description of changes> — description must start with a capital letter.
  • Keep the title under 72 characters (soft requirement).
  • Explain the intention and why the change is needed.
  • Make commit titles meaningful and describe what changed.
  • Do not add code that a later commit rewrites; squash or reorder commits instead.
  • Do not include Fixes #NN in commit messages.

Good examples:

  • frontend: HomeButton: Fix so it navigates to home
  • backend: config: Add enable-dynamic-clusters flag

I noticed the GitHub CI frontend i18n check is failing. It looks like the translation files need to be updated. You can run cd frontend && npm run i18n locally to update them and then commit the result.

How to update translation files

Run cd frontend && npm run i18n to regenerate the translation files. Commit the updated files alongside your other changes.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (3)

frontend/src/components/App/TokenExpiryNotification.tsx:84

  • If a subsequent poll returns a successful response that omits tokenExpiry (e.g., after a silent token refresh that switches the backend to a non-JWT/proxy auth path, or for an auth provider that stops returning the claim), the existing tokenExpiry state value is never cleared. The component will continue to count down toward the old expiry and may auto-logout based on stale data. Consider explicitly resetting tokenExpiry to null when the new response is successful but tokenExpiry is missing.
      if (result.tokenExpired) {
        setTokenExpired(true);
      } else if (result.data?.tokenExpiry !== null && result.data?.tokenExpiry !== undefined) {
        setTokenExpiry(result.data.tokenExpiry);
      }

frontend/src/components/App/TokenExpiryNotification.tsx:138

  • Auto-logout fires on the first poll if the backend returns 401, including the case where the user navigated to a Headlamp route with a cluster context but has never authenticated to that cluster (e.g., cold load on a route under /c/<cluster>/... before the auth flow has run). In that scenario the user gets immediately bounced through logout(), which can produce a confusing UX vs. the normal "you are not authenticated" flow. Consider only triggering auto-logout when the user previously had a valid session (i.e., we've successfully observed tokenExpired: false at least once during this mount), rather than on the very first response.
  React.useEffect(() => {
    if (!tokenExpired) return;
    if (clusterName) {
      logout(clusterName);
    }
  }, [tokenExpired, clusterName]);

frontend/src/components/App/TokenExpiryNotification.tsx:82

  • The two null/undefined checks on result.data?.tokenExpiry are redundant — optional chaining on a null data already yields undefined. This can be simplified to a single != null (loose equality) or typeof result.data?.tokenExpiry === 'number' check, which also defensively guards against unexpected non-numeric values from the backend.
      } else if (result.data?.tokenExpiry !== null && result.data?.tokenExpiry !== undefined) {

Comment thread backend/pkg/auth/auth.go Outdated
Comment thread frontend/src/components/App/TokenExpiryNotification.tsx
Comment thread frontend/src/lib/auth.ts Outdated
Comment thread frontend/src/lib/auth.ts
@prabindersinghh prabindersinghh changed the title frontend: warn user before token expiry and auto-logout on session end frontend: TokenExpiryNotification: Key polling on cluster name not pathname May 18, 2026
@prabindersinghh prabindersinghh changed the title frontend: TokenExpiryNotification: Key polling on cluster name not pathname frontend: warn user before token expiry and auto-logout on session end May 18, 2026
@prabindersinghh prabindersinghh force-pushed the fix/token-expiry-5112 branch from 2ab10e6 to d9dbdde Compare May 18, 2026 07:38
@prabindersinghh
Copy link
Copy Markdown
Contributor Author

Addressed all Copilot feedback in the latest push (bed0c63):

-Reverted time.Now().Before → time.Now().After for correct expiry boundary semantics.
-Simplified ClusterMeResult discriminated union, collapsed duplicate tokenExpired: false variants into { tokenExpired: false; data: ClusterMeResponse | null }
-fetchClusterMe export is intentional for testability via injectable fetchClusterMeFn, not intended as public plugin API; happy to move it or add a docstring if preferred.
-Transient failure hardening (consecutive 401s before logout) left as follow-up per current scope, happy to include if you'd prefer it in this PR.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

frontend/src/components/App/TokenExpiryNotification.tsx:84

  • When a successful /me response is returned without a tokenExpiry field (for example after a silent token refresh that produces a non-JWT credential, or when the auth provider stops returning an exp claim), the existing tokenExpiry state is not cleared. The countdown and the local clock-based check at lines 125–128 will continue to operate against the old expiry value, and may fire a false-positive warning banner — or even auto-logout — even though the current token has no known expiry. Consider resetting tokenExpiry to null when the field is absent from a successful response.
      if (result.tokenExpired) {
        setTokenExpired(true);
      } else if (result.data?.tokenExpiry !== null && result.data?.tokenExpiry !== undefined) {
        setTokenExpiry(result.data.tokenExpiry);
      }

frontend/src/components/App/TokenExpiryNotification.tsx:149

  • At line 149 the render path calls getCluster() again instead of reusing the clusterName variable already computed at line 55 (and used as the effect dependency). Re-reading the cluster here can desynchronize from the effect (which keyed its setup on the earlier clusterName) if getCluster() ever returns a different value between the two reads on the same render. For consistency and to avoid surprising behavior, use clusterName here.
  if (!showOnRoute || !getCluster()) {

Comment thread frontend/src/components/App/TokenExpiryNotification.tsx
Comment thread frontend/src/lib/auth.ts
Comment thread backend/pkg/auth/auth.go
@prabindersinghh
Copy link
Copy Markdown
Contributor Author

Addressed latest Copilot feedback in efb150d:

-Polling interval now clears immediately on token expiry via tokenExpiredRef, no further /me requests after logout is triggered
-fetchClusterMe marked @internal in JSDoc, not a stable plugin API
-auth.go expiry path acknowledged as safe; happy to harden if preferred

@illume illume requested a review from Copilot May 19, 2026 14:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Comment thread frontend/src/lib/auth.ts
Comment thread frontend/src/components/App/TokenExpiryNotification.tsx Outdated
Comment thread frontend/src/components/App/TokenExpiryNotification.tsx
Copy link
Copy Markdown
Contributor

@illume illume left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution.

There are some open Copilot review comments — could you take a look at them? Please mark each one as resolved once you've addressed it.

@k8s-ci-robot k8s-ci-robot added cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. and removed cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels May 19, 2026
@prabindersinghh prabindersinghh force-pushed the fix/token-expiry-5112 branch from 5387837 to 122e781 Compare May 19, 2026 19:16
@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. and removed cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels May 19, 2026
@prabindersinghh
Copy link
Copy Markdown
Contributor Author

All Copilot comments addressed and resolved:

-JSDoc @internal tag merged into main docstring block
-setTimeout delay clamped to 2^31-1 to prevent 32-bit overflow for long-lived tokens
-tokenExpiredRef and interval cleared in local-expiry path

Ready for re-review.

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

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. needs review oidc Issue related to OIDC size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants