Authorization Channel
Notifikasi Panel dikirim melalui channel private yang dinamai berdasarkan penerimanya. Private berarti Laravel akan bertanya kepada aplikasi apakah socket ini boleh mendengarkan channel tersebut, satu kali ketika subscription dibuka. Callback itulah seluruh model keamanan untuk realtime notification, dan letaknya di routes/channels.php milik aplikasi, bukan di package — karena rule tersebut berbicara tentang user model Anda, bukan tentang Panel.
Contoh minimal yang berfungsi
<?php
// routes/channels.php
declare(strict_types=1);
use Illuminate\Support\Facades\Broadcast;
/*
* The panel's toasts are broadcast on the signed-in user's own private
* channel. This is the rule that makes that safe: a socket may only subscribe
* to the channel whose id is its own, whatever the frontend asks for.
*/
Broadcast::channel(
'App.Models.User.{id}',
static fn ($user, int|string $id): bool => hash_equals((string) $user->getAuthIdentifier(), (string) $id),
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Itulah file yang dimuat verbatim oleh test suite package. Tidak ada konfigurasi lain yang diperlukan untuk notifikasi Panel.
Nama channel
Nama channel dibangun di server, pada satu tempat:
namespace PandaPanel\Broadcasting;
public static function channelFor(Authenticatable $user): string
{
return 'App.Models.User.'.$user->getAuthIdentifier();
}2
3
4
5
6
use PandaPanel\Broadcasting\PanelNotification;
PanelNotification::channelFor($user); // 'App.Models.User.7'2
3
Kedua broadcast event membungkusnya dalam Illuminate\Broadcasting\PrivateChannel, sehingga nama pada wire memiliki prefix private-:
$event = new PanelNotification($user, 'Export finished', 'success');
$event->broadcastOn()[0]->name; // 'private-App.Models.User.7'2
3
Pattern Broadcast::channel() ditulis tanpa prefix tersebut karena Laravel menghapusnya sebelum melakukan matching. Nama ini juga merupakan nama default Laravel dan sengaja tidak diubah: notifikasi yang dibroadcast oleh bagian lain dari aplikasi dapat tiba pada channel yang sama yang sudah didengarkan Panel.
Panel::getBroadcastChannel() menentukan apakah nama channel dikirim ke browser atau tidak:
public function getBroadcastChannel(?Authenticatable $user): ?string
{
return $this->broadcasting && $user !== null
? PanelNotification::channelFor($user)
: null;
}2
3
4
5
6
Guest menerima null, sehingga tidak ada channel yang perlu disubscribe daripada mengirim channel yang pasti ditolak.
Bagaimana pemeriksaan sebenarnya berjalan
- Echo membuka subscription dan melakukan POST ke
/broadcasting/authdengan session cookie. - Laravel mencocokkan
private-App.Models.User.7dengan pattern yang didaftarkan lalu mengikat{id}menjadi7. - Callback dijalankan dengan authenticated user dan parameter hasil binding.
truemenandatangani subscription;false— atau tidak ada authenticated user — menghasilkan 403 dan socket tidak pernah bergabung.
Nama channel yang diminta client harus dianggap tidak tepercaya, dan memang itulah tujuannya: server mengirimkan nama channel milik user tersebut, tetapi callback tetap menolak nama milik user lain apa pun yang dikirim frontend.
Key non-integer dan custom user model
Callback default membandingkan string karena channelFor() juga memakai auth identifier sebagai string. Sesuaikan perbandingan dengan cara model Anda mengidentifikasi user:
// UUID primary keys — never cast a UUID to int.
Broadcast::channel(
'App.Models.User.{id}',
static fn ($user, string $id): bool => hash_equals((string) $user->getAuthIdentifier(), $id),
);2
3
4
5
Apa pun yang dibandingkan, bandingkan dengan nilai yang sama dengan output channelFor(): getAuthIdentifier(), yaitu value dari kolom auth identifier model — biasanya primary key.
Jika user model Anda bukan App\Models\User, nama channel tetap App.Models.User. Itu hanyalah string, bukan referensi class, dan Panel membangunnya dari authenticated user tanpa memedulikan class user tersebut. Pertahankan pattern ini kecuali Anda memiliki alasan untuk mengganti kedua sisinya, yang tidak dapat dilakukan untuk Panel karena channelFor() tidak configurable.
Guard
Broadcast::channel() menerima options array, dan Panel tidak mengubah perilakunya. Panel yang menggunakan non-default guard perlu menyebutkan guard tersebut:
Broadcast::channel(
'App.Models.User.{id}',
static fn ($user, int|string $id): bool => hash_equals((string) $user->getAuthIdentifier(), (string) $id),
['guards' => ['web', 'admin']],
);2
3
4
5
Tanpa konfigurasi itu Laravel memeriksa default guard, sehingga user yang hanya authenticated melalui admin akan ditolak.
Mendaftarkan file
routes/channels.php dimuat oleh aplikasi, bukan package:
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
channels: __DIR__.'/../routes/channels.php',
)
// …2
3
4
5
6
7
php artisan install:broadcasting menulis file tersebut sekaligus baris registrasinya. Package tidak mengirimkan keduanya: aplikasi yang sudah meng-authorize channel ini — seperti aplikasi Laravel yang sudah pernah melakukan broadcast notification — akan berakhir dengan dua definisi rule yang saling bertabrakan jika package ikut menulisnya.
Mengujinya
Baca kembali callback dari broadcaster daripada menulis ulang rule di test. Inilah pola yang digunakan test suite package:
use Illuminate\Support\Facades\Broadcast;
it('lets a user onto their own channel only', function (): void {
$other = User::factory()->create();
$callback = Broadcast::connection()->getChannels()->get('App.Models.User.{id}');
expect($callback)->not->toBeNull()
->and($callback($this->admin, $this->admin->getKey()))->toBeTrue()
->and($callback($this->admin, $other->getKey()))->toBeFalse();
});2
3
4
5
6
7
8
9
10
11
Test yang hanya meng-assert salinan rule miliknya sendiri masih dapat lolos ketika rule sebenarnya pada aplikasi salah.
Hal yang tidak di-authorize oleh channel ini
Channel hanya melindungi jalur realtime. Notification center memiliki jalur dan model authorization yang berbeda dan sama sekali tidak membaca routes/channels.php:
| Jalur | Di-authorize oleh |
|---|---|
| Websocket subscription | callback di routes/channels.php |
GET/POST /{panel}/notifications* | query scope — semuanya dimulai dari $request->user() |
| URL action pada stored notification | route/controller tujuan URL tersebut |
Endpoint notification tidak membutuhkan policy karena tidak ada id pada request yang dapat menjangkau row milik user lain: id milik user lain hanya tidak menghasilkan match, bukan 403. Lihat Notification center.
Hal yang perlu diperhatikan
channels.phpyang tidak ada menyebabkan feature loss yang nyaris silent. Subscription menghasilkan 403 di network tab, composable tidak menampilkannya sebagai error, dan Panel tetap berjalan tanpa toast.- Callback dijalankan pada HTTP request, bukan pada socket. Callback memiliki session sehingga
auth()->user()dan route model binding bekerja normal; callback tidak memiliki Panel context karena/broadcasting/authbukan route Panel. - Jangan cast channel id ke integer kecuali auth identifier Anda dijamin selalu integer. UUID atau string key dapat berubah menjadi
0dan membuka risiko kebocoran channel. Bandingkan representasi string darigetAuthIdentifier()sebagai gantinya. - Authorization channel di-cache per subscription, bukan per event. User yang permission-nya berubah di tengah session tetap mempertahankan socket yang sudah berhasil di-join sampai page direload. Jangan gunakan channel ini untuk mengirim data yang seharusnya dilindungi policy; kirim link, lalu biarkan destination melakukan authorization.
Lihat juga
- Broadcasting — event yang menggunakan channel ini
- Setup Reverb dan Echo — wiring lainnya
- Notification center — model authorization yang lain
- Authorization
- Negative security tests
- Broadcast failures