diff --git a/src/App.jsx b/src/App.jsx
index 52e77a476ebc36be1391dd9b8a04d616d86ade90..a8c3e9f7ff4d8ede2d91e7546f7f37e63dd0c533 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -11,7 +11,7 @@ import { loadAnnouncements } from "actions/announcements";
 import { loadConfig } from "actions/global-info";
 import { loadPosts } from "actions/posts";
 import { loadProgram } from "actions/program";
-import { loadMe } from "actions/users";
+import { loadMe, refreshAccessToken } from "actions/users";
 import { initializeWSChannel } from "actions/ws";
 import Footer from "components/Footer";
 import Navbar from "components/Navbar";
@@ -39,7 +39,12 @@ if (process.env.REACT_APP_SENTRY_DSN) {
 
 const ReactHint = ReactHintFactory(React);
 
-const onKeycloakEvent = (event) => {
+const onKeycloakEvent = async (event) => {
+  if (event === "onTokenExpired") {
+    console.warn("[auth] access token expired, attempting refresh");
+    refreshAccessToken();
+  }
+
   if (["onAuthRefreshSuccess", "onAuthSuccess"].includes(event)) {
     Sentry.setUser(keycloak.tokenParsed);
 
@@ -154,6 +159,7 @@ const AuthenticatedApp = () => {
         initConfig={keycloakInitConfig}
         LoadingComponent={LoadingComponent}
         onEvent={onKeycloakEvent}
+        autoRefreshToken={false}
       >
         <Suspense fallback={LoadingComponent}>
           <ConfiguredApp />
diff --git a/src/actions/users.js b/src/actions/users.js
index 3bb8fb4a05bdef364e7df4d5a1b1b7d088bd29de..c3a626698be9b0fde216194336a6317767dad1fd 100644
--- a/src/actions/users.js
+++ b/src/actions/users.js
@@ -1,7 +1,10 @@
+import * as Sentry from "@sentry/react";
 import { createAsyncAction, errorResult, successResult } from "pullstate";
 
 import { fetch } from "api";
-import { AuthStore } from "stores";
+import keycloak from "keycloak";
+import { AuthStore, PostStore } from "stores";
+import { updateWindowPosts } from "utils";
 
 export const loadMe = createAsyncAction(
   /**
@@ -89,3 +92,28 @@ export const inviteToJitsi = createAsyncAction(
     }
   }
 );
+
+export const refreshAccessToken = async () => {
+  try {
+    await keycloak.updateToken(300);
+    console.info("[auth] access token refreshed");
+  } catch (exc) {
+    console.warn(
+      "[auth] could not refresh the access token, refresh token possibly expired, logging out"
+    );
+
+    Sentry.setUser(null);
+
+    AuthStore.update((state) => {
+      state.isAuthenticated = false;
+      state.user = null;
+      state.showJitsiInvitePopup = false;
+      state.jitsiPopupDimissed = false;
+    });
+
+    PostStore.update((state) => {
+      state.filters.showPendingProposals = false;
+      updateWindowPosts(state);
+    });
+  }
+};
diff --git a/src/index.js b/src/index.js
index 63f13377424e58ef3ba12bee37c012fac2069cb8..7565c0e112c1d42a168d6dde095b924fde71d552 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,11 +2,21 @@ import React from "react";
 import ReactDOM from "react-dom";
 import ReactModal from "react-modal";
 
+import { refreshAccessToken } from "actions/users";
+
 import App from "./App";
 import * as serviceWorker from "./serviceWorker";
 
 const root = document.getElementById("root");
 
+function handleVisibilityChange() {
+  if (!document.hidden) {
+    refreshAccessToken();
+  }
+}
+
+document.addEventListener("visibilitychange", handleVisibilityChange, false);
+
 ReactDOM.render(
   <React.StrictMode>
     <App />