Token Refresh
Why proactive refresh matters
Epic access tokens expire (typically after 3600 seconds). Without proactive refresh, a clinician mid-consultation would see an unexpected 401 error when their token expires. The TokenRefreshFilter prevents this by refreshing 120 seconds before expiry — the clinician never sees the expiry.
TokenRefreshFilter
TokenRefreshFilter is a OncePerRequestFilter applied to /api/** routes:
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
SmartLaunchContext ctx = getContextFromSession(req);
if (ctx != null) {
refreshService.refreshIfNeeded(ctx);
}
chain.doFilter(req, res);
}
TokenRefreshService
@Service
public class TokenRefreshService {
private static final long REFRESH_BUFFER_SECONDS = 120;
public void refreshIfNeeded(SmartLaunchContext ctx) {
Instant threshold = ctx.getExpiresAt()
.minusSeconds(REFRESH_BUFFER_SECONDS);
if (Instant.now().isAfter(threshold)) {
refresh(ctx);
}
}
private void refresh(SmartLaunchContext ctx) {
TokenResponse response = tokenService.refreshTokens(
ctx.getRefreshToken(),
ctx.getFhirBaseUrl());
// Update in place — same session object
ctx.setAccessToken(response.accessToken());
ctx.setExpiresAt(Instant.now()
.plusSeconds(response.expiresIn()));
// Handle refresh token rotation
if (response.refreshToken() != null) {
ctx.setRefreshToken(response.refreshToken());
}
}
}
Refresh token rotation
Epic may return a new refresh token on each refresh (rotation). TokenRefreshService updates ctx.refreshToken when a new one is provided, and retains the old one otherwise.
Session expiry handling
If the refresh token itself is expired (Epic's refresh tokens have their own expiry — typically 8 hours), the token exchange returns an error. TokenRefreshService handles this:
try {
refresh(ctx);
} catch (InvalidGrantException e) {
// Refresh token expired — invalidate session
session.invalidate();
// Caller catches and redirects to /launch?error=session_expired
throw new SessionExpiredException();
}
401 force-refresh
If HAPI FHIR returns 401 despite the proactive refresh (e.g. Epic token was revoked), a FhirClientInterceptor detects the 401 and triggers an immediate refresh before retrying once.
Next: UI Layer →