Olympus Docs
CookbookSessions

Refresh auth on app foreground

User returns to app after a long break, check session

Mobile and PWA apps often go to background for hours. When user returns, refresh the session before letting them act.

Why

  • Session might have expired.
  • Server might have invalidated (admin revoked, account locked).
  • User's role / plan might have changed.

Without refresh: app shows stale state, user takes actions that fail server-side.

Mobile / PWA pattern

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "visible") {
    refreshSession();
  }
});

Triggers when user switches back to your tab / app.

Stale checks

async function refreshSession() {
  try {
    const session = await olympus.toSession();
    if (!session) {
      // Server says no session, redirect to login
      window.location.href = "/login";
      return;
    }
    // Update local state
    setUser(session.identity);
  } catch (err) {
    console.error("session_refresh_failed", err);
    // Fall back to login
    window.location.href = "/login";
  }
}

Optimization: skip if recently checked

let lastCheck = 0;
async function refreshSession() {
  if (Date.now() - lastCheck < 60_000) return;  // skip if checked < 60s ago
  lastCheck = Date.now();
  // ... actually refresh
}

Avoid hammering server.

Web app pattern

For desktop browsers:

let lastActivity = Date.now();

document.addEventListener("mousemove", () => lastActivity = Date.now());
document.addEventListener("keypress", () => lastActivity = Date.now());

window.addEventListener("focus", () => {
  if (Date.now() - lastActivity > 5 * 60_000) {  // idle 5+ min
    refreshSession();
  }
});

Smart: only refresh when user was AFK.

Server response

Server can hint about staleness:

Response headers:
X-Session-Age: 1735  # seconds since session created
X-Refresh-In: 60     # refresh in 60 seconds

Client uses to schedule next refresh.

Sliding sessions

If you want sliding expiration (extends on activity):

async function refreshSession() {
  const session = await olympus.toSession();
  if (session) {
    // Server extends expiry based on this hit
  }
}

setInterval(refreshSession, 5 * 60_000);  // every 5 min

Kratos's whoami doesn't auto-extend by default. Configure:

session:
  earliest_possible_extend: 1h

whoami extends if remaining lifespan < 1h.

Battery considerations

Mobile: frequent network calls drain battery.

For PWAs, lessen:

  • Refresh on visibility change only (not interval).
  • Background sync API for occasional check.
if ("BackgroundSync" in window && document.visibilityState === "hidden") {
  // browser-managed background check
}

Auth state across tabs

When user signs out in one tab, others should know:

// In one tab on logout:
localStorage.setItem("auth_logout", Date.now().toString());

// Other tabs listen:
window.addEventListener("storage", (e) => {
  if (e.key === "auth_logout") {
    window.location.reload();  // re-check auth, will fail, redirect to login
  }
});

Cross-tab sync.

Service Worker token refresh

For PWAs:

self.addEventListener("fetch", async (event) => {
  const response = await fetch(event.request);
  if (response.status === 401) {
    // Try to refresh
    const refreshed = await refreshTokens();
    if (refreshed) {
      // Retry original request
      return fetch(event.request);
    } else {
      // Redirect to login
      return Response.redirect("/login");
    }
  }
  return response;
});

Auto-refresh in background.

Notification when session ends

When session has truly expired (refresh fails):

<Toast type="info">
  You were signed out due to inactivity. <Link href="/login">Sign in</Link> to continue.
</Toast>

Don't just redirect silently. Inform.

Edge: server says token revoked

You refresh; server returns "token_revoked" (admin action, security event).

try {
  await refreshSession();
} catch (err) {
  if (err.code === "token_revoked") {
    showAlert("Your session was ended. This might be due to a security event.");
    redirectTo("/login");
  }
}

Honest message.

Refresh during pending action

User opens a form, AFK for an hour, comes back, submits:

async function submit(formData) {
  try {
    await api.submit(formData);
  } catch (err) {
    if (err.code === "session_expired") {
      // Try to refresh and retry
      await refreshSession();
      await api.submit(formData);  // retry
    } else {
      throw err;
    }
  }
}

Sneaky-good UX: user doesn't see "session expired" if it can be transparently refreshed.

Don't refresh on every request

Per-request introspection is fine on the server. Don't make every API call also refresh-the-session-locally.

Refresh:

  • On app visible.
  • Periodically (5-15 min).
  • On 401 response.

Not:

  • Before every request.
  • Every render.

On this page