skip to content

In Laravel broadcasting, how are private and presence channels authorized with Broadcast::channel, and what must a presence channel callback return?

level: middleimportance: must knowfreq 50%

answer

  1. routes/channels.php callbacks
  2. Echo calls /broadcasting/auth
  3. user first, wildcards after
  4. private: true or false
  5. presence: an array of user data

basics

~20 s

Echo asks /broadcasting/auth before subscribing; Laravel runs the matching Broadcast::channel callback from routes/channels.php with the authenticated user and wildcard values. Private callbacks return true or false; presence callbacks return an array describing the user, or false/null to refuse.

solid answer

~40 s

When Echo subscribes to `private-auctions.42` or `presence-auctions.42`, it first sends the socket ID and channel name to `/broadcasting/auth`, a route registered in the `web` middleware group. Laravel strips the prefix and finds the `Broadcast::channel('auctions.{auction}', ...)` callback in `routes/channels.php`. The callback receives the user from the default guard (or the `guards` option) and the wildcard values, which can use model binding. For a **private** channel it returns `true` to allow or `false` to deny, and a denial is a 403. For a **presence** channel it returns an **array** such as `['id' => $user->id, 'name' => $user->name]`; that data is what other members see in Echo's `here`, `joining` and `leaving` callbacks. A guest is refused before any callback runs, and `php artisan channel:list` lists the callbacks.

code

javascript · 5 lines
javascript
Echo.join(`auction-room.${auctionId}`)
    .here((users) => setWatchers(users))
    .joining((user) => addWatcher(user))
    .leaving((user) => removeWatcher(user))
    .error((error) => console.error(error));

go deeper

for a junior

Recall that routes/channels.php holds Broadcast::channel callbacks and that private and presence channels need them.

for a middle

Explain the /broadcasting/auth round trip, the callback arguments with model binding and guards, and why presence callbacks return arrays.

for a senior

Treat presence data as published to every member, choose private versus presence per feature, and keep channel rules in sync with the app's policies.

for a principal

Set rules for what may ever travel over realtime channels and how channel authorization reuses the app's policies instead of duplicating them.

## Why channels need authorization A **public channel** (`Channel`) can be subscribed to by anyone who knows its name. **Private** (`PrivateChannel`) and **presence** (`PresenceChannel`) channels are for data a visitor must be entitled to: the outbid notice for one bidder, or the room of people watching an invite-only auction. The WebSocket server cannot decide who is entitled, so it asks the Laravel application to sign each subscription. ## The authorization round trip 1. Echo connects to the WebSocket server and receives a **socket ID**. 2. To subscribe to `private-auctions.42`, Echo sends an HTTP request to `/broadcasting/auth` with the socket ID and the channel name. The request carries the session cookie, so it is authenticated like any web request. 3. Laravel removes the `private-` or `presence-` prefix and matches the rest against the patterns registered with `Broadcast::channel()`. 4. If the callback allows it, Laravel returns a signature the WebSocket server verifies; otherwise it responds with **403** and Echo reports an error. The `/broadcasting/auth` route is registered when `routes/channels.php` is wired into `withRouting()` (done by `install:broadcasting`). By default it sits in the `web` middleware group and is exempt from the CSRF check. ## Writing the callbacks Callbacks live in `routes/channels.php`: - the pattern uses `{placeholders}` like a route: `'auctions.{auction}'` - the **first argument** is the authenticated user; the placeholders follow in order - placeholders can use **model binding**, so `Auction $auction` receives the model (implicit binding scoping is not supported here) - the user is resolved through the default guard, or the guards listed in a third argument, `['guards' => ['web', 'admin']]` - if no user is authenticated, the request is refused before the callback runs - large apps can move the logic into a channel class created with `php artisan make:channel AuctionChannel`, whose `join()` method has the same contract ## Private versus presence return values | Channel type | Callback returns to allow | To deny | Extra effect | |---|---|---|---| | `PrivateChannel` | `true` (any truthy value) | `false` | none | | `PresenceChannel` | an array of user info | `false` or `null` | the array is shared with other members | A callback that returns `null` for a private channel is also treated as a refusal, because only a truthy result is accepted. For a presence channel the returned array is sent to every member. Echo's `join()` exposes it: - `here(users)` runs once after joining with the current members - `joining(user)` and `leaving(user)` run as others come and go So the array should contain only what other members may see, such as an ID and a display name, never an email address or role flags. ## The auction example ```php use App\Models\Auction; use App\Models\User; use Illuminate\Support\Facades\Broadcast; Broadcast::channel('bidders.{bidder}', fn (User $user, User $bidder) => $user->is($bidder)); Broadcast::channel('auction-room.{auction}', function (User $user, Auction $auction) { if ($user->can('view', $auction)) { return ['id' => $user->id, 'name' => $user->display_name]; } return false; }); ``` The first gives each bidder a private channel for "you have been outbid" messages. The second is a presence channel showing who is watching an invite-only auction, and it reuses the auction policy instead of duplicating the rule. ## Debugging a refused subscription A 403 from `/broadcasting/auth` in the browser's network tab means Laravel ran the lookup and said no. The usual causes are: - the user is not authenticated on the guard the channel checks, for example a user logged in on an `admin` guard while the channel uses the default one - the pattern in `Broadcast::channel()` does not match the subscribed name, often because of a typo or a stray `private-` prefix - a model-bound placeholder did not resolve, or the callback's rule is simply false for this user - the callback returned `null` for a private channel, which counts as a refusal `php artisan channel:list` shows every registered pattern and its handler, which settles the pattern question quickly. ## Frequent mistakes - returning `true` from a presence callback, which gives other members nothing to identify the user by - putting the channel name with its `private-` prefix into `Broadcast::channel()` - assuming a channel is protected because its name is hard to guess, while broadcasting it as a public `Channel` - returning the whole `User` model from a presence callback and leaking its attributes to every member

  • What happens when a guest's Echo client tries to subscribe to a private channel?
    The `/broadcasting/auth` request has no authenticated user, so Laravel refuses it with a 403 before running any `Broadcast::channel` callback. Echo reports a subscription error and the client receives nothing on that channel.
  • How do you authorize a channel for users logged in through an admin guard?
    Pass the guards as the third argument: `Broadcast::channel('auction-admin.{auction}', $callback, ['guards' => ['web', 'admin']])`. Laravel tries each guard to find the user before running the callback; without the option only the default guard is used.
  • routes/channels.php has grown to dozens of closures; how do you organise it?
    Generate channel classes with `php artisan make:channel AuctionChannel`, put the rule in the class's `join()` method, and register it with `Broadcast::channel('auction-room.{auction}', AuctionChannel::class)`. `join()` takes the same arguments and returns the same values as a closure, and the class is resolved from the container, so it can receive dependencies such as a policy service.

saying these in an interview costs you the question

  • A presence channel callback should return true, like a private channel.
  • Channel patterns in Broadcast::channel must include the private- prefix.
  • The WebSocket server decides who may join a private channel on its own.
  • Returning the whole User model from a presence callback is harmless.
  • Presence channels need no Broadcast::channel callback because members are public.