Autentikasi Dua Faktor
Fortify tetap memiliki second-factor flow pada login POST:
- TOTP;
- recovery code;
- passkeys.
PandaBear tidak menduplikasi flow tersebut.
PandaBear menambahkan dua kemampuan:
- Panel dapat mewajibkan account memiliki second factor sebelum boleh menggunakan Panel.
- PandaBear menyediakan Email Code sebagai alternatif second factor untuk user yang tidak menggunakan authenticator app.
Contoh Minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin;
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
final class AdminPanelProvider
extends PanelProvider
{
public function panel(
Panel $panel
): Panel {
return $panel
->path('admin')
->auth()
->requireTwoFactor();
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Signed-in user tanpa second factor akan diarahkan ke Security Settings.
it(
'holds a user without a second factor at the security page',
function (): void {
$this
->actingAs(
User::factory()
->create()
)
->get('/admin')
->assertRedirect(
route(
'panel.admin.pages.settings-security',
absolute: false
)
);
}
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Setelah account memiliki:
- TOTP;
- Email Code;
- Passkey;
redirect tersebut berhenti.
Apa yang Dihitung sebagai Second Factor
RequireTwoFactor memeriksa dalam urutan:
| Factor | Check | User Model |
|---|---|---|
| TOTP | hasEnabledTwoFactorAuthentication() | Fortify trait |
| Email Code | EmailCodeFactor::isEnabledFor() | two_factor_email_confirmed_at |
| Passkey | passkeys()->exists() | Passkey trait |
Checks menggunakan method_exists() untuk trait-provided behavior.
Model yang tidak mendukung sebuah feature dianggap:
factor tidak tersediabukan exception.
Passkey sengaja dianggap cukup karena hardware/WebAuthn key merupakan strong authentication factor.
Panel API
public function requireTwoFactor(
bool $required = true
): self;
public function requiresTwoFactor():
bool;2
3
4
5
6
Contoh:
Panel::make(
'admin'
)
->requireTwoFactor()
->requiresTwoFactor();
// true2
3
4
5
6
7
Panel::make(
'kiosk'
)
->requireTwoFactor(false)
->requiresTwoFactor();
// false2
3
4
5
6
7
Default:
falseConfiguration dapat menggunakan expression:
$panel->requireTwoFactor(
app()->isProduction()
);2
3
Settings Requirement
User membutuhkan tempat untuk setup second factor.
API:
public function settings(
bool $settings = true
): self;
public function hasSettings():
bool;2
3
4
5
6
Default settings menyediakan:
Profile
Security
Appearance2
3
Jangan menggabungkan:
settings(false)dengan:
requireTwoFactor()tanpa menyediakan alternative setup flow.
Middleware
PandaPanel\Http\Middleware\RequireTwoFactorotomatis diregistrasikan.
Stack:
[
...$panel->getMiddleware(),
ResolvePanel::class
. ':'
. $panel->getId(),
RequireTwoFactor::class
. ':'
. $panel->getId(),
RequireEmailCode::class
. ':'
. $panel->getId(),
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
Signature:
public function handle(
Request $request,
Closure $next,
?string $panelId = null
): Response;2
3
4
5
Panel id diberikan eksplisit.
Middleware juga dapat digunakan sendiri:
use PandaPanel\Http\Middleware\RequireTwoFactor;
Route::get(
'/reports/payroll',
PayrollController::class
)
->middleware([
'auth',
RequireTwoFactor::class
. ':admin',
]);2
3
4
5
6
7
8
9
10
11
Middleware melewatkan request jika:
- Panel tidak diketahui;
- Panel tidak mewajibkan second factor;
- tidak ada signed-in user;
- user sudah memiliki factor;
- request adalah Security Settings;
- request adalah standalone Page milik Panel.
Jika Panel mewajibkan factor tetapi tidak memiliki Security Page:
403Framework fail-closed.
Jika setup diperlukan:
return redirect()
->route(
$panel->routeName(
'pages.'
. SecuritySettings::slug()
)
)
->with(
'warning',
'Set up two-factor authentication to continue.'
);2
3
4
5
6
7
8
9
10
11
Tempat User Mengaktifkan Factor
Page:
{panel}/settings/securityroute:
panel.{id}.pages.settings-securityPage berada di balik:
RequirePasswordJadi user harus membuktikan password sebelum mengubah security settings.
Props:
| Prop | Sumber |
|---|---|
canManageTwoFactor | Fortify |
canManagePasskeys | Fortify |
passkeys | User relation |
passwordRules | PandaBear helper |
emailCodeEnabled | Email Code factor |
emailCodeUrls | Panel routes |
twoFactorEnabled | TOTP state |
requiresConfirmation | Fortify config |
Page menggunakan:
InteractsWithTwoFactorStatedan memanggil:
ensureStateIsValid()agar abandoned TOTP setup tidak tertinggal dalam state separuh aktif.
TOTP write tetap menuju Fortify.
Email Code Bawaan Panel
Email Code adalah one-time code yang dikirim ke email account.
Fitur ini bukan bagian dari Login POST.
Behavior:
Login via Fortify
↓
Session created
↓
RequireEmailCode
↓
session belum verified?
↓
challenge
↓
kode benar
↓
session marker2
3
4
5
6
7
8
9
10
11
12
13
Session baru selalu perlu challenge lagi.
Enable / Disable
Routes berada di balik RequirePassword.
public function enable(
Request $request
): RedirectResponse;
public function disable(
Request $request
): RedirectResponse;2
3
4
5
6
7
URLs:
/admin/two-factor/enable
/admin/two-factor/disable2
Enable:
$user
->forceFill([
'two_factor_email_confirmed_at'
=> now(),
])
->save();
$request
->session()
->put(
PanelTwoFactorController::SESSION_KEY,
now()->timestamp
);2
3
4
5
6
7
8
9
10
11
12
13
Current session langsung dianggap lolos.
Disable:
column = null
↓
forget pending challenge
↓
hapus session marker2
3
4
5
Challenge
Controller methods:
public function challenge(
Request $request
): Response;
public function send(
Request $request
): RedirectResponse;
public function verify(
Request $request
): RedirectResponse;2
3
4
5
6
7
8
9
10
11
Routes:
GET {panel}/two-factor/challenge
POST {panel}/two-factor/send
POST {panel}/two-factor/verify2
3
Challenge Page props:
| Prop | Arti |
|---|---|
panel | Panel definition |
sentTo | Masked email |
retryAfter | Detik sampai resend tersedia |
send() dapat menghasilkan ValidationException jika rate limit terlampaui.
Jika user tidak memiliki notify():
500verify() memvalidasi:
[
'code' => [
'required',
'string',
'digits:6',
],
]2
3
4
5
6
7
Setelah sukses:
regenerate session
↓
set SESSION_KEY
↓
redirect intended2
3
4
5
Session key:
PanelTwoFactorController::SESSION_KEY;
// panel.mfa.email.confirmed_at2
3
RequireEmailCode
Middleware ini berbeda dari RequireTwoFactor.
RequireTwoFactor memeriksa:
ACCOUNT ENROLMENTsedangkan RequireEmailCode memeriksa:
CURRENT SESSIONJadi account yang mengaktifkan Email Code tetap mendapatkan challenge per-session meskipun Panel tidak menggunakan:
requireTwoFactor()Middleware menyimpan:
url.intendeddan setelah challenge user kembali ke tujuan semula.
EmailCodeFactor
public static function isEnabledFor(
?object $user
): bool;2
3
Membaca:
$user
->getAttributes()[
'two_factor_email_confirmed_at'
]2
3
4
secara langsung.
Missing attribute dianggap:
falseA non-Eloquent model juga false.
EmailCodeChallenge
API:
| Method | Return |
|---|---|
issue() | Plain code atau null |
verify() | bool |
pending() | bool |
secondsUntilNextSend() | int |
forget() | void |
Contoh:
$challenge =
app(
EmailCodeChallenge::class
);
$code =
$challenge->issue(
$user
);
if ($code === null) {
return back()
->withErrors([
'code' =>
'Try again in '
. $challenge
->secondsUntilNextSend(
$user
)
. 's.',
]);
}
$user->notify(
new TwoFactorCode(
$code
)
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Security constants:
| Rule | Value |
|---|---|
| Code | 6 digit |
| Lifetime | 10 menit |
| Storage | hashed Cache value |
| Sends | 5/jam/user |
| Guesses | 5/menit/user |
| Single use | Ya |
Successful verify juga membersihkan guess limiter.
Notification
PandaPanel\Notifications\TwoFactorCodeimplements:
ShouldQueueMail tidak disimpan di Panel notification bell karena ini merupakan credential.
Migration
Package menambahkan:
two_factor_email_confirmed_atke users.
Publish:
php artisan vendor:publish --tag=panda-panel-migrations
php artisan migrate2
Cast:
protected function casts(): array
{
return [
'two_factor_email_confirmed_at'
=> 'datetime',
];
}2
3
4
5
6
7
Route Reference
| Route | Verb | Path | Extra |
|---|---|---|---|
| challenge | GET | {panel}/two-factor/challenge | — |
| send | POST | {panel}/two-factor/send | — |
| verify | POST | {panel}/two-factor/verify | — |
| enable | POST | {panel}/two-factor/enable | RequirePassword |
| disable | POST | {panel}/two-factor/disable | RequirePassword |
Testing
Challenge:
Notification::fake();
$user =
User::factory()
->create();
$user
->forceFill([
'two_factor_email_confirmed_at'
=> now(),
])
->save();
$this
->actingAs($user)
->get('/coded')
->assertRedirect(
route(
'panel.coded.auth.two-factor.challenge',
absolute: false
)
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Skip challenge:
$this
->actingAs($user)
->withSession([
PanelTwoFactorController::SESSION_KEY
=> now()->timestamp,
])
->get('/admin')
->assertOk();2
3
4
5
6
7
8
Password confirmation:
$this
->actingAs($user)
->withSession([
'auth.password_confirmed_at'
=> now()->timestamp,
])
->post(
'/admin/two-factor/disable'
)
->assertRedirect();2
3
4
5
6
7
8
9
10
Rate limiter:
RateLimiter::clear(
'panel.mfa.email.send.'
. $user->getKey()
);
RateLimiter::clear(
'panel.mfa.email.attempt.'
. $user->getKey()
);2
3
4
5
6
7
8
9
Untuk test enrollment TOTP:
$user
->forceFill([
'two_factor_secret'
=> encrypt(
'secret'
),
'two_factor_confirmed_at'
=> now(),
])
->save();2
3
4
5
6
7
8
9
10
11
Hal yang Perlu Diperhatikan
requireTwoFactor()memeriksa enrollment, bukan apakah factor benar-benar dipakai saat login.- TOTP login verification tetap concern Fortify.
- Email Code adalah exception karena
RequireEmailCodememeriksa current session. - Semua standalone Page saat ini dikecualikan dari
RequireTwoFactor. - Sensitive functionality sebaiknya berada di Resource atau diberi check tambahan.
settings(false)danrequireTwoFactor()bertentangan.- Enable Email Code langsung memenuhi current session.
- Challenge Page mengirim email sebagai side effect GET jika belum ada pending code.
send()500 jika model tidak memilikinotify().- Masked address dihitung saat runtime, tidak disimpan.
- Security constants Email Code tidak configurable.