Why Keycloak?
Healthcare applications need proper authentication. Basic auth and API keys don't cut it when you're dealing with PHI-adjacent systems. InteropNimbus uses Keycloak for enterprise-grade SSO because:
- Standards-based — OAuth 2.0 and OpenID Connect
- Self-hosted — runs alongside the rest of the infrastructure
- Multi-tenant — one Keycloak instance serves multiple applications (FhirHub, InteropNimbus, etc.)
- Enterprise features — LDAP/AD federation, MFA, session management
Shared Keycloak Instance
InteropNimbus shares a Keycloak instance with FhirHub. Both applications use the same realm but different clients:
export const authConfig = {
url: import.meta.env.VITE_KEYCLOAK_URL ?? 'https://auth.interopnimbus.davidle.dev',
realm: import.meta.env.VITE_KEYCLOAK_REALM ?? 'interopnimbus',
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID ?? 'interopnimbus-frontend',
} as const
The Keycloak URL, realm, and client ID are all configurable via environment variables, injected at build time through Vite.
PKCE Authentication Flow
InteropNimbus uses PKCE (Proof Key for Code Exchange) with S256, the recommended flow for public clients (SPAs):
keycloak
.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri:
`${window.location.origin}/silent-check-sso.html`,
pkceMethod: 'S256',
checkLoginIframe: false,
})
.then((authenticated) => {
if (authenticated) { syncAuthStore(); removeSplash(); setReady(true) }
else { keycloak.login() }
})
.catch(() => { keycloak.clearToken(); keycloak.login() })
Key Configuration Choices
onLoad: 'check-sso'— checks if the user is already authenticated via Keycloak's session, enabling seamless SSO across applicationssilentCheckSsoRedirectUri— uses a hidden iframe for silent token checks, avoiding visible redirectspkceMethod: 'S256'— uses SHA-256 for the PKCE challenge, preventing authorization code interception attackscheckLoginIframe: false— disables the legacy login iframe check, which conflicts with modern CSP headers
Token Lifecycle
The AuthProvider manages the full token lifecycle:
- Init — check-sso to see if user is already logged in
- Login — redirect to Keycloak if not authenticated
- Sync — extract user info from the ID token and populate the Zustand auth store
- Refresh — automatically refresh tokens 30 seconds before expiry
- Logout — clear tokens and redirect back to Keycloak login
keycloak.onTokenExpired = () => {
keycloak.updateToken(30).catch(() => {
useAuthStore.getState().clearAuth()
keycloak.login()
})
}
If the token refresh fails (e.g., the session was revoked server-side), the user is redirected back to login rather than seeing a broken state.
Role-Based Access
Keycloak realm roles are mapped to the frontend auth store:
const roles = (keycloak.realmAccess?.roles ?? []) as AuthRole[]
useAuthStore.getState().setAuth({
sub: parsed.sub ?? '',
email: parsed.email as string,
name: parsed.name as string,
roles,
})
This enables role-based UI rendering — certain features or views can be gated based on the user's assigned roles in Keycloak.