Authentication

Learn how Soketify authentication works, how to manage your API credentials, and how to implement authentication endpoints for private and presence channels.

How Authentication Works

Soketify uses the same authentication model as Pusher. There are two distinct contexts where authentication applies:

  1. Server-to-Soketify API — Your backend server authenticates with Soketify using your App ID, App Key, and App Secret to trigger events and query channel state via the REST API. This uses HMAC-SHA256 signature authentication on each request.
  2. Client-to-Your-Server (Channel Auth) — When a client subscribes to a private or presence channel, the Pusher client library makes an HTTP request to your auth endpoint to verify that the user is allowed to join. Your server generates a signature using the App Secret and returns it to the client.

Public channels (those without a private- or presence- prefix) do not require any authentication. Any client with your App Key can subscribe to them.

App Key vs App Secret

CredentialPurposeWhere to UsePublic?
App KeyIdentifies your app on WebSocket connectionsClient-side (browser, mobile app)Yes
App SecretSigns API requests and channel auth responsesServer-side onlyNo
App IDIdentifies your app in REST API URLsServer-side onlyNo

Never Expose Your App Secret

The App Secret must never appear in client-side code, browser JavaScript bundles, public repositories, or anywhere accessible to end users. If your secret is compromised, rotate it immediately in the Dashboard.

Managing Secrets in the Dashboard

You can view and manage your app credentials from the Soketify Dashboard:

  1. Dashboard Go to Dashboard and select your app.
  2. Navigate to the tab. Settings
  3. Your App ID, App Key, and App Secret are displayed in the Credentials section.
  4. Use the copy button to copy any credential to your clipboard.

Store your credentials as environment variables on your server. A common pattern is to use a .env file:

.env
bash
SOKETIFY_APP_ID=your-app-id
SOKETIFY_APP_KEY=your-app-key
SOKETIFY_APP_SECRET=your-app-secret

Secret Rotation Strategy

Rotating your App Secret is a best practice for maintaining security. Soketify supports multiple concurrent active secrets, so you can rotate with zero downtime in three steps:

  1. Create a new secret — In the Dashboard, add a new secret on your app's settings page. It becomes active immediately, alongside your existing secret. Both are now valid at the same time.
  2. Deploy the new secret — Update your servers to sign with the new secret and roll them out at your own pace. During this grace period both the old and the new secret are accepted, so requests and channel authentications signed with either one keep working. No connection is dropped.
  3. Retire the old secret — Once every server is signing with the new secret, deactivate or delete the old one in the Dashboard. From that point only the new secret is accepted.

Rotation Best Practices

Rotate your secrets on a regular schedule (e.g., every 90 days) and immediately if you suspect a compromise. Store secrets in a secrets manager rather than directly in code or configuration files.

Private Channel Authentication

When a client subscribes to a channel with the private- prefix, the Pusher client library automatically sends an HTTP POST request to your auth endpoint. Here is the flow:

Browser
pusher-js
Your Server
/pusher/auth
Soketify
ws.soketify.com
POST /pusher/auth
socket_id + channel_name
verify user session
sign: HMAC_SHA256(secret, socket_id:channel)
{ auth: "key:sig" }
subscribe + auth token
subscription_succeeded

Auth Endpoint Implementation (Node.js / Express)

server.js
javascript
1const express = require("express");
2const Pusher = require("pusher");
3
4const app = express();
5app.use(express.urlencoded({ extended: false }));
6
7const pusher = new Pusher({
8  appId: process.env.SOKETIFY_APP_ID,
9  key: process.env.SOKETIFY_APP_KEY,
10  secret: process.env.SOKETIFY_APP_SECRET,
11  host: "api.soketify.com",
12  port: "443",
13  useTLS: true,
14  cluster: "default",
15});
16
17app.post("/pusher/auth", (req, res) => {
18  // Verify user is logged in (use your own auth middleware)
19  const user = req.session?.user;
20  if (!user) {
21    return res.status(403).json({ error: "Not authenticated" });
22  }
23
24  const { socket_id, channel_name } = req.body;
25
26  // Optional: check the user has access to this specific channel
27  // e.g., "private-user-123" should only be accessible by user 123
28  const userId = channel_name.replace("private-user-", "");
29  if (userId !== user.id) {
30    return res.status(403).json({ error: "Not authorized for this channel" });
31  }
32
33  // Generate the auth signature
34  const authResponse = pusher.authorizeChannel(socket_id, channel_name);
35  res.json(authResponse);
36});
37
38app.listen(3000);

Client Configuration

Configure the Pusher client to use your auth endpoint:

client.js
javascript
1const pusher = new Pusher("YOUR_APP_KEY", {
2  wsHost: "ws.soketify.com",
3  wsPort: 443,
4  wssPort: 443,
5  forceTLS: true,
6  disableStats: true,
7  enabledTransports: ["ws", "wss"],
8  cluster: "default",
9  // Point to your auth endpoint
10  channelAuthorization: {
11    endpoint: "/pusher/auth",
12    transport: "ajax",
13  },
14});
15
16const privateChannel = pusher.subscribe("private-user-123");
17privateChannel.bind("pusher:subscription_succeeded", () => {
18  console.log("Successfully subscribed to private channel");
19});
20privateChannel.bind("pusher:subscription_error", (error) => {
21  console.error("Auth failed:", error);
22});

Rules for your authorization endpoint

Your authorization endpoint holds your app secret, and a signature it produces is the only thing a node checks. Everything an attacker needs is therefore decided by your endpoint, before any request reaches us. Three rules, in order of how often they are broken:

  1. 1.Never sign a channel name you were handed. Check the name against a pattern or a list, and check that THIS user is allowed to join THAT channel. An endpoint that signs whatever arrives hands anyone who can reach it a signature for every private and presence channel of your app, including channels of other users. This is the single most common Pusher misconfiguration and it applies to Soketify identically.
  2. 2.Require a session before you sign. The endpoint must sit behind your own authentication. An endpoint reachable without a session is a public signing service for your app secret.
  3. 3.Keep channel authorization and user authentication separate. They are different endpoints (channelAuthorization and userAuthentication in pusher-js) and they answer different questions: may this connection join this channel, versus who is this connection. Do not route both through one handler that signs whatever it is asked to.

Info

The official server libraries already enforce rule 1 for you: pusher (Node), pusher-php-server and the Python pusher package all raise on a name that is not a legal channel name instead of signing it. Verified against those three. Hand-rolled HMAC signing does not, which is why the wrong version below is the one worth looking at.

Wrong: a signing oracle

server.js
javascript
// Do NOT do this. It signs any string it is given.
app.post("/pusher/auth", (req, res) => {
  const { socket_id, channel_name } = req.body;
  const signature = crypto
    .createHmac("sha256", process.env.SOKETIFY_APP_SECRET)
    .update(`${socket_id}:${channel_name}`)
    .digest("hex");
  res.json({ auth: `${process.env.SOKETIFY_APP_KEY}:${signature}` });
});

Right: validate, then sign

server.js
javascript
1app.post("/pusher/auth", (req, res) => {
2  const user = req.session?.user;                 // rule 2: a session first
3  if (!user) return res.sendStatus(403);
4
5  const { socket_id, channel_name } = req.body;
6
7  // rule 1: only the channels this endpoint is willing to speak about, and
8  // only the ones THIS user belongs to. Anything else is refused, not signed.
9  const room = /^private-room-([0-9]+)$/.exec(channel_name)?.[1];
10  if (!room) return res.sendStatus(403);
11  if (!userIsMemberOfRoom(user.id, room)) return res.sendStatus(403);
12
13  // authorizeChannel also refuses illegal channel names by itself.
14  res.json(pusher.authorizeChannel(socket_id, channel_name));
15});
16
17// rule 3: signing a USER is a different endpoint, and never signs a name that
18// arrived in the request. Only needed if User authentication is on for the app.
19app.post("/pusher/user-auth", (req, res) => {
20  const user = req.session?.user;
21  if (!user) return res.sendStatus(403);
22  res.json(pusher.authenticateUser(req.body.socket_id, { id: user.id }));
23});

Why rule 1 also protects user identity

Pusher signs two different strings with the same app secret: socket_id:channel_name to authorize a channel, and socket_id::user::user_data to sign a user in. A channel literally named :user::{"id":"admin"} therefore produces the SAME string as a signin for the user admin, so an authorization for that name is a valid signin token for that user. This is a property of the Pusher protocol, not of Soketify, and we keep both strings byte for byte because changing them would break every official SDK. It is only reachable if your endpoint signs a name it should have refused.

What Soketify does about it on our side:

  • That channel can never be joined. A colon is not a legal character in a channel name, so a node refuses the subscription regardless of the signature.
  • pusher:signin is refused unless you turn User authentication on for the app. An app that never signs users in has no identity to steal, whatever your endpoint signs.
  • We watch for the evidence. A subscribe carrying a valid signature for an illegal channel name can only come from your secret, so when a node sees one we raise it on your app's page in the dashboard: your endpoint is signing without validating.

Presence Channel Authentication

Presence channels work similarly to private channels but require additional user information. When the client subscribes to a presence- channel, your auth endpoint must return user data along with the signature.

Auth Endpoint for Presence Channels

server.js
javascript
1app.post("/pusher/auth", (req, res) => {
2  const user = req.session?.user;
3  if (!user) {
4    return res.status(403).json({ error: "Not authenticated" });
5  }
6
7  const { socket_id, channel_name } = req.body;
8
9  // For presence channels, provide user data
10  if (channel_name.startsWith("presence-")) {
11    const presenceData = {
12      user_id: user.id,
13      user_info: {
14        name: user.name,
15        email: user.email,
16        avatar: user.avatar,
17      },
18    };
19
20    const authResponse = pusher.authorizeChannel(
21      socket_id,
22      channel_name,
23      presenceData
24    );
25    return res.json(authResponse);
26  }
27
28  // For private channels
29  if (channel_name.startsWith("private-")) {
30    const authResponse = pusher.authorizeChannel(socket_id, channel_name);
31    return res.json(authResponse);
32  }
33
34  res.status(400).json({ error: "Invalid channel" });
35});

Using Presence Data on the Client

presence-client.js
javascript
1const presence = pusher.subscribe("presence-chat-room");
2
3// Fired when subscription succeeds - contains all current members
4presence.bind("pusher:subscription_succeeded", (members) => {
5  console.log("Total members:", members.count);
6
7  // Iterate over all members
8  members.each((member) => {
9    console.log(member.id, member.info.name);
10  });
11});
12
13// Fired when a new member joins
14presence.bind("pusher:member_added", (member) => {
15  console.log(`${member.info.name} joined`);
16});
17
18// Fired when a member leaves
19presence.bind("pusher:member_removed", (member) => {
20  console.log(`${member.info.name} left`);
21});

Auth Response Format Reference

For reference, here are the exact response formats your auth endpoint should return. The Pusher server libraries handle this for you, but it is useful to understand the format for debugging.

Private Channel Response

Response
json
{
  "auth": "APP_KEY:HMAC_SHA256_SIGNATURE"
}

// The signature is computed as:
// HMAC_SHA256(app_secret, socket_id + ":" + channel_name)

Presence Channel Response

Response
json
{
  "auth": "APP_KEY:HMAC_SHA256_SIGNATURE",
  "channel_data": "{"user_id":"123","user_info":{"name":"Alice"}}"
}

// The signature is computed as:
// HMAC_SHA256(app_secret, socket_id + ":" + channel_name + ":" + channel_data)

Info

Note that channel_data is a JSON string, not a nested object. The Pusher server libraries handle this serialization automatically.

Next Steps