Auth Flows
The frontend supports two authentication modes and a demo mode that bypasses auth entirely.
Mode Selection
Section titled “Mode Selection”| Mode | Activated By | Backend Required |
|---|---|---|
| Standard (username/password) | Default | Yes |
| Keycloak SSO (PLGrid) | REACT_APP_ALT_AUTH=plg | Yes + Keycloak |
| Demo (no auth) | REACT_APP_TARGET=demo | No |
Standard Authentication
Section titled “Standard Authentication”Login Flow
Section titled “Login Flow”// AuthService.tsx — simplifiedconst login = async (username: string, password: string) => { const response = await ky.post('auth/login', { json: { username, password }, credentials: 'include' // sends/receives httpOnly cookies });
const { accessExp } = await response.json();
// Store user info in localStorage for persistence across refreshes localStorage.setItem('user', JSON.stringify({ username }));
// Start auto-refresh timer startRefreshTimer(accessExp);};Auto-Refresh
Section titled “Auto-Refresh”The UI auto-refreshes the access token at 1/3 of its lifetime:
const startRefreshTimer = (accessExp: number) => { const now = Date.now() / 1000; const ttl = accessExp - now; const refreshIn = (ttl / 3) * 1000; // milliseconds
setTimeout(async () => { const response = await ky.get('auth/refresh', { credentials: 'include' }); const { accessExp: newExp } = await response.json(); startRefreshTimer(newExp); }, refreshIn);};This creates a self-sustaining refresh loop. If the refresh fails (e.g., refresh token expired), the user is logged out.
Logout
Section titled “Logout”const logout = async () => { await ky.delete('auth/logout', { credentials: 'include' }); localStorage.removeItem('user'); // Reset the UI to the login tab};Session Persistence
Section titled “Session Persistence”On page load, the UI checks localStorage for a saved user and attempts a token refresh:
// On app loadconst savedUser = localStorage.getItem('user');if (savedUser) { try { await refreshToken(); // Session restored } catch { localStorage.removeItem('user'); // Session expired, show login }}Keycloak SSO
Section titled “Keycloak SSO”Configuration
Section titled “Configuration”KeycloakAuthService.tsx initializes the Keycloak JS SDK:
const keycloak = new Keycloak({ url: config.keycloakBaseUrl, // REACT_APP_KEYCLOAK_BASE_URL realm: config.keycloakRealm, // REACT_APP_KEYCLOAK_REALM clientId: config.keycloakClientId // REACT_APP_KEYCLOAK_CLIENT_ID});Init Options
Section titled “Init Options”keycloak.init({ onLoad: 'check-sso', pkceMethod: 'S256', silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`, checkLoginIframe: false});check-sso— checks for existing session without forcing loginpkceMethod: 'S256'— PKCE with SHA-256 challenge (prevents authorization code interception)silentCheckSsoRedirectUri— invisible iframe for session checks without page reload
Token Exchange
Section titled “Token Exchange”After Keycloak authentication, the frontend exchanges the Keycloak token with the YAPTIDE backend:
const exchangeKeycloakToken = async () => { const response = await ky.post('auth/keycloak', { headers: { Authorization: `Bearer ${keycloak.token}` }, credentials: 'include' });
const { accessExp } = await response.json(); startRefreshTimer(accessExp);};The backend validates the Keycloak token, creates/updates the user, and issues local JWT cookies.
Auto-Refresh
Section titled “Auto-Refresh”Keycloak tokens are refreshed independently of the YAPTIDE tokens:
// Refresh Keycloak token when < 5 minutes remainingkeycloak.onTokenExpired = () => { keycloak.updateToken(300).then((refreshed) => { if (refreshed) { // Re-exchange with backend exchangeKeycloakToken(); } });};PLGrid Service Check
Section titled “PLGrid Service Check”The UI checks the Keycloak token for PLGrid service claims:
const hasYaptideAccess = keycloak.tokenParsed?.PLG_YAPTIDE_ACCESS === true;
if (!hasYaptideAccess) { // Show dialog: "You need to enroll in the YAPTIDE PLGrid service" showServiceRejectionDialog();}Demo Mode
Section titled “Demo Mode”When REACT_APP_TARGET=demo:
const demoMode = process.env.REACT_APP_TARGET === 'demo';
// AuthService.tsxif (config.demoMode) { // Skip all auth logic // User is "anonymous" // No backend communication return;}In demo mode:
- The login tab is hidden
- No API calls are made
- Only Geant4 Wasm simulations work
- No results are persisted
Server Reachability
Section titled “Server Reachability”AuthService.tsx includes a reachability poller that periodically checks if the backend is accessible:
const checkServerReachable = async () => { try { await ky.get('auth/status', { credentials: 'include', timeout: 5000 }); setServerReachable(true); } catch { setServerReachable(false); }};If the server becomes unreachable, the UI shows a notification and disables simulation submission (remote simulations only — Geant4 Wasm continues to work).
The authKy Client
Section titled “The authKy Client”AuthService exports an authKy HTTP client — a pre-configured ky instance with:
const authKy = ky.create({ prefixUrl: config.backendUrl, credentials: 'include', hooks: { afterResponse: [snakeToCamelTransformer] }});All backend API calls in the frontend use authKy to ensure cookies are sent and responses are camelCased.