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:
- 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.
- 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
| Credential | Purpose | Where to Use | Public? |
|---|---|---|---|
App Key | Identifies your app on WebSocket connections | Client-side (browser, mobile app) | Yes |
App Secret | Signs API requests and channel auth responses | Server-side only | No |
App ID | Identifies your app in REST API URLs | Server-side only | No |
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:
- Dashboard Go to Dashboard and select your app.
- Navigate to the tab. Settings
- Your App ID, App Key, and App Secret are displayed in the Credentials section.
- 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:
SOKETIFY_APP_ID=your-app-id
SOKETIFY_APP_KEY=your-app-key
SOKETIFY_APP_SECRET=your-app-secretSecret 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:
- 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.
- 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.
- 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:
Auth Endpoint Implementation (Node.js / Express)
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:
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.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.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.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
Wrong: a signing oracle
// 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
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
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
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
{
"auth": "APP_KEY:HMAC_SHA256_SIGNATURE"
}
// The signature is computed as:
// HMAC_SHA256(app_secret, socket_id + ":" + channel_name)Presence Channel Response
{
"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
- Learn about all channel types and when to use each one
- Explore the REST API for triggering events from your server
- Migrate from Pusher if you are switching from an existing Pusher setup