diff --git a/package-lock.json b/package-lock.json
index 825b7e4153e9548048930504c3b46a0856f3d2bc..519c08389b35057159711a64cdf76c1337899ffa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4316,6 +4316,11 @@
         "randomfill": "^1.0.3"
       }
     },
+    "crypto-js": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz",
+      "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg=="
+    },
     "css": {
       "version": "2.2.4",
       "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
diff --git a/package.json b/package.json
index 529a9a8761eacc1995254bea7984b1b356e395a5..b1e1bec8b9ba25340747dffe550885d547f4af18 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
     "@rooks/use-window-size": "^4.5.0",
     "@sentry/react": "^5.23.0",
     "classnames": "^2.2.6",
+    "crypto-js": "^4.0.0",
     "date-fns": "^2.16.1",
     "immer": "^7.0.15",
     "keycloak-js": "^10.0.2",
diff --git a/src/actions/users.js b/src/actions/users.js
index 49cf58bb99164c92d36367dbadd39648c2d35bf9..3f5f5ef55d92fd48c4aecdf4dfd7eee28398405a 100644
--- a/src/actions/users.js
+++ b/src/actions/users.js
@@ -27,6 +27,7 @@ export const loadMe = createAsyncAction(
             state.user.id = result.payload.id;
             state.user.group = result.payload.group;
             state.user.isBanned = result.payload.is_banned;
+            state.user.secret = result.payload.secret || "";
           }
         });
       }
diff --git a/src/ws/connection.js b/src/ws/connection.js
index 4882d1b5ef831c535322d4f8a46e0bdd992ad09b..19ced7f0d14ba7b86c43b11eec7da2b236cd1bab 100644
--- a/src/ws/connection.js
+++ b/src/ws/connection.js
@@ -1,6 +1,8 @@
+import hex from "crypto-js/enc-hex";
+import hmacSHA1 from "crypto-js/hmac-sha1";
 import WaitQueue from "wait-queue";
 
-import { GlobalInfoStore } from "stores";
+import { AuthStore, GlobalInfoStore } from "stores";
 
 import { handlers } from "./handlers";
 
@@ -44,6 +46,18 @@ function Worker() {
   };
 }
 
+const buildKeepalivePayload = async () => {
+  const { user } = AuthStore.getRawState();
+  const payload = user && user.id ? user.id.toString() : "";
+  const signature = user.secret ? hmacSHA1(payload, user.secret) : null;
+
+  return {
+    event: "KEEPALIVE",
+    payload,
+    sig: hex.stringify(signature),
+  };
+};
+
 export const connect = ({ onConnect }) => {
   return new Promise((resolve, reject) => {
     const worker = Worker();
@@ -61,14 +75,17 @@ export const connect = ({ onConnect }) => {
         state.connectionState = "connected";
       });
       console.log("[ws] Connected.");
-      ws.send("CONNECT");
 
-      keepAliveInterval = setInterval(() => {
-        ws.send("KEEPALIVE");
+      const sendKeepalive = async () => {
+        ws.send(JSON.stringify(await buildKeepalivePayload()));
         console.debug("[ws] Sending keepalive.");
-      }, 30 * 1000);
+      };
+
+      sendKeepalive();
+
+      keepAliveInterval = setInterval(sendKeepalive, 15 * 1000);
 
-      const self = { ws, worker };
+      const self = { ws, worker, sendKeepalive };
 
       if (onConnect) {
         return onConnect(self).then(() => resolve(self));
diff --git a/typings/cf2021.d.ts b/typings/cf2021.d.ts
index fa510f244c8899b72ccb2c54cca831ba32e4eb8c..4b9d6284b1f7a44f79bb0707050d50cbbae745f6 100644
--- a/typings/cf2021.d.ts
+++ b/typings/cf2021.d.ts
@@ -46,6 +46,7 @@ declare namespace CF2021 {
       id?: number;
       isBanned?: boolean;
       group?: string;
+      secret?: string;
     };
   }