Skip to content

Comprehensive Auth Hardening — Token Refresh, Route Guards, Session Timeout, Error Handling - #16

Open
Flashl3opard wants to merge 1 commit into
openMF:devfrom
Flashl3opard:feat/auth-hardening
Open

Flashl3opard wants to merge 1 commit into
openMF:devfrom
Flashl3opard:feat/auth-hardening

Conversation

@Flashl3opard

Copy link
Copy Markdown
Member

EPIC PHEE-363

Summary

Implemented a complete auth hardening suite across 6 phases to make the Keycloak ROPC flow production-ready. The root issue was that keycloak-js wasn't actually receiving tokens after login, preventing it from managing refresh and expiry. Now tokens are properly hydrated, silent refresh works on a proactive interval, protected routes enforce auth checks on every navigation, and 401/403 responses are handled gracefully.

Changes

Phase 1: Token Hydration (Root Cause Fix)

  • src/modules/auth/Login.tsx — After ROPC login, now calls keycloak.init({ token, refreshToken, idToken }) to give keycloak-js the tokens it needs
  • src/lib/keycloak/keycloak.ts — New persistTokens() / clearTokens() helpers centralize token storage (was scattered across 14+ raw localStorage calls)

Phase 2: Silent Token Refresh

  • src/lib/keycloak/refresh.ts (new) — Wraps keycloak.updateToken(300) with retry-once-after-2s fallback, built on keycloak-js's own mechanism
  • src/lib/keycloak/KeycloakProvider.tsx — Refresh interval proactively refreshes tokens when <5 min remain

Phase 3: Route Guarding

  • src/components/shared/ProtectedRoute.tsx (new) — Guards every navigation, checks auth state + token expiry on every route change (not just initial load)
  • src/main.tsx — Wrapped in AuthRoot; all non-login routes protected

Phase 4: Session Timeout + Logout

  • src/lib/keycloak/useIdleTimer.ts (new) — 15-min idle threshold, warns at 13 min with modal
  • src/components/shared/SessionTimeoutModal.tsx (new) — Styled to match existing dialogs; "Stay Logged In" refreshes token, timeout auto-logs out
  • Logout flow — Now actually revokes token server-side (keycloak-js finally holds a real refresh token to revoke)

Phase 5: 401/403 Error Handling

  • src/lib/api/authInterceptors.ts (new) — Axios interceptors handle:
    • 401 Unauthorized — Attempts silent refresh via refresh.ts, retries request once (guarded by _retry flag). On refresh failure, clears tokens + redirects to login.
    • 403 Forbidden — Emits 'forbidden' event (picked up by ForbiddenListener.tsx, redirects to / + shows toast)
    • Request layer — Injects Platform-TenantId + Authorization Bearer headers, pings activity tracker
  • src/lib/api/client.ts & src/lib/api/g2pConfig.ts — Both now use createAuthInterceptors()
  • src/components/shared/ForbiddenListener.tsx (new) — Reacts to 'forbidden' event; redirects + notifies user

Phase 6: Security Documentation

  • README.md — Added "Security" section documenting:
    • Token storage (in-memory only, no localStorage for access/refresh tokens)
    • ROPC flow rationale (in-app login, trusted client)
    • Refresh mechanism (proactive, expires at <5 min)
    • Session timeout (15 min idle)
    • Error handling (401 retry, 403 redirect)

Utilities

  • src/lib/events.ts (new) — Event bus for 'forbidden' | 'activity' events (decouples interceptors from UI)

Testing

New test files:

  • src/lib/keycloak/refresh.test.ts — 5 tests (refresh success, refresh failure, retry logic)
  • src/lib/keycloak/useIdleTimer.test.ts — 4 tests (idle detection, warn state, reset)
  • src/lib/api/authInterceptors.test.ts — 4 tests (401→retry, 401→fail, 403→event, redirectToLogin export)

Results:

  • 33/33 tests passing
  • tsc --noEmit — clean, no type errors
  • lint — baseline 13 pre-existing issues (unchanged)

Key Insights

  1. Root cause was hydration — keycloak-js couldn't refresh/expire tokens because it never received them after login. Calling keycloak.init() with actual tokens fixed everything downstream.
  2. No hand-rolled refresh needed — keycloak-js's updateToken() handles refresh properly once it has the tokens; we just wrapped it with retry logic.
  3. Event bus decouples concerns — Interceptors emit events rather than managing UI directly; components listen independently.
  4. Proactive > reactive — Refreshing on a 5-min interval before expiry beats waiting for 401 errors.

Impact

  • For operations staff: Seamless sessions, no unexpected logouts mid-workflow, clear feedback on timeout.
  • For developers: Patterns are now clear and tested; new modules just use the protected route + interceptors.
  • For security: Tokens are only in memory, refresh is server-validated, 401/403 are handled gracefully.

Remaining

  • Amount formatting bug (PHEE-416) — separate fix, ready to go
  • Extensibility documentation — will document these auth patterns as gold standard
  • Blog post — final writeup due Aug 31

Checklist

  • All 6 phases implemented and tested
  • 33/33 tests passing
  • TypeScript clean
  • Security documented
  • No breaking changes to existing components
  • Ready for code review

…sion timeout, 401/403 handling, security review
@Flashl3opard
Flashl3opard requested review from a team and a lite review from Copilot September 11, 2026 11:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@DavidH-1

Copy link
Copy Markdown
Contributor

could this be raised against the new repo as we have merged this repo across ready for release. So all additions should now be to https://github.com/openMF/paymenthub-ee-operationsui-react/

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants