AuthorizationPrivate channels

Authorization endpoints

When a client subscribes to a private- or presence- channel, the SDK posts to your auth endpoint and expects a signed token. Below are working examples in the most common server stacks.

What the endpoint receives

Form-encoded body:

FieldDescription
socket_idThe connection’s unique id, assigned by Mawjly when the socket opens. Looks like 123.456789.
channel_nameThe name the client is trying to subscribe to.

What the endpoint returns

JSON:

{ "auth": "YOUR_KEY:hex_hmac_sha256_signature" }

The signature is HMAC-SHA256 of the string "<socket_id>:<channel_name>" using your app secret.

For presence channels, also include a channel_data field (see the next page).


Laravel (automatic)

You don’t write the endpoint manually — Laravel ships it. Just define your channels in routes/channels.php:

use Illuminate\Support\Facades\Broadcast;
 
Broadcast::channel('user.{id}', function ($user, $id) {
    return $user->id === (int) $id;
});
 
Broadcast::channel('order.{id}', function ($user, $id) {
    return Order::find($id)?->user_id === $user->id;
});

Laravel signs with PUSHER_APP_SECRET and Mawjly verifies. Done.


Node.js (Express)

import express from 'express';
import Pusher from 'pusher';
 
const app = express();
app.use(express.urlencoded({ extended: true }));
 
const pusher = new Pusher({
  appId: process.env.MAWJLY_APP_ID!,
  key: process.env.MAWJLY_KEY!,
  secret: process.env.MAWJLY_SECRET!,
  cluster: 'sa',
  host: 'ws-sa.mawjly.com',
  port: '443',
  useTLS: true,
});
 
app.post('/pusher/auth', requireAuth, (req, res) => {
  const { socket_id, channel_name } = req.body;
  const userId: string = res.locals.userId;
 
  // Permission check
  if (channel_name === `private-user.${userId}`) {
    const auth = pusher.authorizeChannel(socket_id, channel_name);
    return res.json(auth);
  }
 
  return res.sendStatus(403);
});

PHP (vanilla)

<?php
require __DIR__ . '/vendor/autoload.php';
session_start();
 
if (empty($_SESSION['user_id'])) {
    http_response_code(401);
    exit;
}
 
$userId = (int) $_SESSION['user_id'];
$channel = $_POST['channel_name'] ?? '';
$socketId = $_POST['socket_id'] ?? '';
 
if ($channel !== "private-user.{$userId}") {
    http_response_code(403);
    exit;
}
 
$pusher = new Pusher\Pusher(
    'YOUR_APP_KEY',
    'YOUR_APP_SECRET',
    'YOUR_APP_ID',
    [
        'host'    => 'ws-sa.mawjly.com',
        'port'    => 443,
        'scheme'  => 'https',
        'cluster' => 'sa',
        'useTLS'  => true,
    ],
);
 
header('Content-Type: application/json');
echo $pusher->authorizeChannel($channel, $socketId);

Python (Flask)

from flask import Flask, request, session, jsonify, abort
import pusher
 
app = Flask(__name__)
p = pusher.Pusher(
    app_id='YOUR_APP_ID',
    key='YOUR_APP_KEY',
    secret='YOUR_APP_SECRET',
    cluster='sa',
    host='ws-sa.mawjly.com',
    port=443,
    ssl=True,
)
 
@app.post('/pusher/auth')
def auth():
    user_id = session.get('user_id')
    if not user_id:
        abort(401)
 
    channel = request.form['channel_name']
    socket_id = request.form['socket_id']
 
    if channel != f'private-user.{user_id}':
        abort(403)
 
    return jsonify(p.authenticate(channel=channel, socket_id=socket_id))

Common pitfalls

  • Returning 200 OK with an empty / wrong body → SDK retries forever. Always return either the signed token or a 403.
  • Hashing the wrong order — must be socket_id:channel_name, not the other way around.
  • Generating the signature on the client (don’t — your secret would leak).
  • CORS. The auth endpoint must accept the request method/headers your client sends. Most SDKs use POST with application/x-www-form-urlencoded.