Skip to content

Cara Implementasi Laravel Fortify ​

Panduan langkah demi langkah implementasi Fortify — headless authentication backend untuk Laravel. Fortify menyediakan semua route & logic auth (login, register, reset password, 2FA) tanpa UI — kamu yang bikin tampilannya sendiri.


Instalasi Fortify ​

bash
composer require laravel/fortify
php artisan fortify:install
php artisan migrate

Perintah fortify:install akan:

  • Membuat app/Actions/Fortify/ — action classes (CreateNewUser, ResetUserPassword, dll) ✅
  • Membuat app/Providers/FortifyServiceProvider.php ✅
  • Menerbitkan config/fortify.php ✅
  • Membuat migration database ✅

Konfigurasi Features ​

Di config/fortify.php, atur fitur yang ingin diaktifkan:

php
<?php

use Laravel\Fortify\Features;

'features' => [
    Features::registration(),
    Features::resetPasswords(),
    Features::emailVerification(),
    Features::twoFactorAuthentication([
        'confirm' => true,
        'confirmPassword' => true,
    ]),
    Features::passkeys([
        'confirmPassword' => true,
    ]),
],
FeatureFungsi
registration()Registrasi user baru
resetPasswords()Lupa password & reset
emailVerification()Verifikasi email
twoFactorAuthentication()2FA dengan TOTP (Google Authenticator, dll)
passkeys()Login tanpa password (Face ID, Touch ID, Windows Hello)

Untuk menonaktifkan fitur, tinggal koment atau hapus dari array.

Disable Views Mode ​

Jika Fortify dipasangkan dengan SPA (React/Vue), kamu bisa matikan route view-nya:

php
// config/fortify.php
'views' => false,

Membuat View (Frontend) ​

Karena Fortify tidak menyediakan UI, kamu harus bikin view sendiri. Semua view dikustomisasi via closure di FortifyServiceProvider.

Login View ​

php
<?php

namespace App\Providers;

use Laravel\Fortify\Fortify;
use Illuminate\Support\ServiceProvider;

class FortifyServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Fortify::loginView(function () {
            return view('auth.login');
        });
    }
}

View resources/views/auth/login.blade.php:

html
<form method="POST" action="/login">
    @csrf
    <input type="email" name="email" placeholder="Email" required>
    <input type="password" name="password" placeholder="Password" required>
    <input type="checkbox" name="remember" id="remember">
    <label for="remember">Ingat saya</label>
    <button type="submit">Login</button>
</form>

@if ($errors->any())
    <div>{{ $errors->first() }}</div>
@endif

Register View ​

php
Fortify::registerView(function () {
    return view('auth.register');
});

Form mengirim POST ke /register dengan field: name, email, password, password_confirmation.

Forgot Password View ​

php
Fortify::requestPasswordResetLinkView(function () {
    return view('auth.forgot-password');
});

Form mengirim POST ke /forgot-password dengan field email.

Reset Password View ​

php
Fortify::resetPasswordView(function (Request $request) {
    return view('auth.reset-password', ['request' => $request]);
});

Form mengirim POST ke /reset-password dengan field: email, password, password_confirmation, token (dari route).

Email Verification View ​

php
Fortify::verifyEmailView(function () {
    return view('auth.verify-email');
});

Tampilkan pesan "Cek email kamu untuk verifikasi". Sertakan tombol POST ke /email/verification-notification untuk kirim ulang.

Password Confirmation View ​

php
Fortify::confirmPasswordView(function () {
    return view('auth.confirm-password');
});

Form mengirim POST ke /user/confirm-password dengan field password.

Two-Factor Challenge View ​

php
Fortify::twoFactorChallengeView(function () {
    return view('auth.two-factor-challenge');
});

Form mengirim POST ke /two-factor-challenge dengan field code (TOTP) atau recovery_code.


Custom Actions ​

Saat registrasi dan reset password, Fortify memanggil action classes. Semua ada di app/Actions/Fortify/.

CreateNewUser ​

Ubah validasi atau tambah field registrasi:

php
<?php

namespace App\Actions\Fortify;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;

class CreateNewUser implements CreatesNewUsers
{
    public function create(array $input): User
    {
        Validator::make($input, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'max:255', 'unique:users'],
            'phone' => ['required', 'string', 'max:20'], // custom field
            'password' => $this->passwordRules(),
        ])->validate();

        return User::create([
            'name' => $input['name'],
            'email' => $input['email'],
            'phone' => $input['phone'], // custom field
            'password' => Hash::make($input['password']),
        ]);
    }

    protected function passwordRules(): array
    {
        return ['required', 'string', 'min:8', 'confirmed'];
    }
}

ResetUserPassword ​

php
<?php

namespace App\Actions\Fortify;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;

class ResetUserPassword implements ResetsUserPasswords
{
    public function reset(User $user, array $input): void
    {
        Validator::make($input, [
            'password' => $this->passwordRules(),
        ])->validate();

        $user->forceFill([
            'password' => Hash::make($input['password']),
        ])->save();
    }

    protected function passwordRules(): array
    {
        return ['required', 'string', 'min:8', 'confirmed'];
    }
}

Custom Authentication ​

Ganti logic autentikasi (misal login via username, bukan email):

php
<?php

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Laravel\Fortify\Fortify;

public function boot(): void
{
    Fortify::authenticateUsing(function (Request $request) {
        $user = User::where('email', $request->email)->first();

        if ($user && Hash::check($request->password, $user->password)) {
            return $user;
        }

        return null;
    });
}

Custom Redirect ​

Ubah redirect setelah login/logout:

php
<?php

use Laravel\Fortify\Contracts\LogoutResponse;

public function register(): void
{
    $this->app->instance(LogoutResponse::class, new class implements LogoutResponse {
        public function toResponse($request)
        {
            return redirect('/goodbye');
        }
    });
}

Atur home di config/fortify.php untuk redirect setelah login:

php
'home' => '/dashboard',

Custom Authentication Pipeline ​

Fortify memproses login melalui pipeline. Kamu bisa kustomisasi:

php
<?php

use Laravel\Fortify\Actions\AttemptToAuthenticate;
use Laravel\Fortify\Actions\CanonicalizeUsername;
use Laravel\Fortify\Actions\EnsureLoginIsNotThrottled;
use Laravel\Fortify\Actions\PrepareAuthenticatedSession;
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
use Laravel\Fortify\Features;
use Laravel\Fortify\Fortify;
use Illuminate\Http\Request;

Fortify::authenticateThrough(function (Request $request) {
    return array_filter([
        config('fortify.limiters.login') ? null : EnsureLoginIsNotThrottled::class,
        config('fortify.lowercase_usernames') ? CanonicalizeUsername::class : null,
        Features::enabled(Features::twoFactorAuthentication())
            ? RedirectIfTwoFactorAuthenticatable::class
            : null,
        AttemptToAuthenticate::class,
        PrepareAuthenticatedSession::class,
    ]);
});

Rate Limiting ​

Fortify otomatis throttle percobaan login. Kustomisasi di FortifyServiceProvider:

php
<?php

use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Cache\RateLimiting\Limit;

RateLimiter::for('login', function ($request) {
    return Limit::perMinute(5)->by($request->email . $request->ip());
});

Two-Factor Authentication ​

Persiapan Model ​

php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    use Notifiable, TwoFactorAuthenticatable;
}

Enable 2FA ​

1. User klik "Enable 2FA" → POST ke /user/two-factor-authentication

2. Tampilkan QR Code (Blade):

php
{{ $request->user()->twoFactorQrCodeSvg() }}

Atau via API (JavaScript):

js
fetch('/user/two-factor-qr-code')
    .then(res => res.json())
    .then(data => console.log(data.svg));

3. User input kode dari Google Authenticator → POST ke /user/confirmed-two-factor-authentication

4. Tampilkan Recovery Codes (Blade):

php
@foreach ((array) $request->user()->recoveryCodes() as $code)
    <code>{{ $code }}</code><br>
@endforeach

Login dengan 2FA ​

Setelah login sukses, Fortify otomatis redirect ke /two-factor-challenge. User input kode TOTP atau recovery code.

Disable 2FA ​

DELETE request ke /user/two-factor-authentication.


Passkeys (WebAuthn) ​

Fortify mendukung login tanpa password menggunakan Face ID, Touch ID, Windows Hello, atau hardware security key.

Persiapan Model ​

php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\Contracts\PasskeyUser;
use Laravel\Fortify\PasskeyAuthenticatable;

class User extends Authenticatable implements PasskeyUser
{
    use Notifiable, PasskeyAuthenticatable;
}

Konfigurasi ​

Di config/fortify.php:

php
'passkeys' => [
    'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST),
    'allowed_origins' => [config('app.url')],
    'user_handle_secret' => config('app.key'),
    'timeout' => 60000,
],

JavaScript Client ​

Install package:

bash
npm install @laravel/passkeys

Register passkey:

js
import { Passkeys } from "@laravel/passkeys";

await Passkeys.register({ name: "MacBook Pro" });

Login dengan passkey:

js
await Passkeys.verify();

Confirm password dengan passkey:

js
await Passkeys.verify({
    routes: {
        options: "/passkeys/confirm/options",
        submit: "/passkeys/confirm",
    },
});

React / Vue / Svelte Helpers ​

js
// React
import { usePasskeys } from "@laravel/passkeys/react";

// Vue
import { usePasskeys } from "@laravel/passkeys/vue";

// Svelte
import { usePasskeys } from "@laravel/passkeys/svelte";

Endpoint Passkeys ​

MethodEndpointFungsi
GET/passkeys/login/optionsAmbil challenge untuk login
POST/passkeys/loginLogin dengan passkey
GET/passkeys/confirm/optionsAmbil challenge untuk konfirmasi
POST/passkeys/confirmKonfirmasi password dengan passkey
GET/user/passkeys/optionsAmbil challenge untuk registrasi
POST/user/passkeysRegister passkey baru
DELETE/user/passkeys/{passkey}Hapus passkey

Fortify + Sanctum (Stack SPA) ​

Ini stack paling umum untuk SPA (Next.js, Vue, React) + Laravel backend:

KomponenTugas
FortifyRoute & logic: register, login, reset password, verifikasi email, 2FA
SanctumSession auth (cookie) untuk SPA, API token untuk mobile
Frontend SPAUI auth sendiri (React/Vue) + konsumsi route Fortify

Konfigurasi ​

php
// config/fortify.php — disable views karena SPA handle UI sendiri
'views' => false,
php
// bootstrap/app.php — aktifkan stateful API Sanctum
->withMiddleware(function (Middleware $middleware): void {
    $middleware->statefulApi();
})

Alur Auth SPA + Fortify + Sanctum ​

SPA (React)                    Laravel (Fortify + Sanctum)
   │                                   │
   │── GET /sanctum/csrf-cookie ──────→│ CSRF protection
   │←── Set cookie XSRF-TOKEN ─────────│
   │                                   │
   │── POST /register ────────────────→│ Fortify: buat user
   │   (name, email, password)         │ Sanctum: session cookie
   │←── 201 + Set session cookie ──────│
   │                                   │
   │── POST /login ───────────────────→│ Fortify: auth
   │   (email, password)               │ Sanctum: session cookie
   │←── 200 + Set session cookie ──────│
   │                                   │
   │── GET /api/user ─────────────────→│ Sanctum: cek session
   │   (cookie otomatis)               │
   │←── { user: ... } ────────────────│

Route Fortify (Yang Tersedia Otomatis) ​

MethodRouteDeskripsi
GET/loginTampilkan form login
POST/loginLogin
POST/logoutLogout
GET/registerTampilkan form registrasi
POST/registerRegistrasi
GET/forgot-passwordForm lupa password
POST/forgot-passwordKirim link reset
GET/reset-password/{token}Form reset password
POST/reset-passwordReset password
GET/email/verifyNotifikasi verifikasi email
GET/email/verify/{id}/{hash}Verifikasi email
POST/email/verification-notificationKirim ulang verifikasi
GET/user/confirm-passwordForm konfirmasi password
POST/user/confirm-passwordKonfirmasi password
GET/two-factor-challengeForm 2FA
POST/two-factor-challengeVerifikasi 2FA
POST/user/two-factor-authenticationEnable 2FA
DELETE/user/two-factor-authenticationDisable 2FA
GET/user/two-factor-qr-codeQR code 2FA
GET/user/two-factor-recovery-codesRecovery codes
POST/user/two-factor-recovery-codesRegenerate recovery codes
POST/user/confirmed-two-factor-authenticationConfirm 2FA

Testing Fortify ​

PHPUnit ​

php
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class AuthenticationTest extends TestCase
{
    use RefreshDatabase;

    public function test_users_can_authenticate(): void
    {
        $user = User::factory()->create([
            'email' => 'user@example.com',
            'password' => bcrypt('password'),
        ]);

        $response = $this->post('/login', [
            'email' => 'user@example.com',
            'password' => 'password',
        ]);

        $this->assertAuthenticated();
        $response->assertRedirect('/dashboard');
    }

    public function test_users_can_not_authenticate_with_invalid_password(): void
    {
        $user = User::factory()->create([
            'email' => 'user@example.com',
            'password' => bcrypt('password'),
        ]);

        $this->post('/login', [
            'email' => 'user@example.com',
            'password' => 'wrong-password',
        ]);

        $this->assertGuest();
    }

    public function test_users_can_register(): void
    {
        $response = $this->post('/register', [
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => 'password',
            'password_confirmation' => 'password',
        ]);

        $this->assertAuthenticated();
        $response->assertRedirect('/dashboard');
    }

    public function test_email_verification(): void
    {
        $user = User::factory()->create([
            'email_verified_at' => null,
        ]);

        $response = $this->actingAs($user)
            ->post('/email/verification-notification');

        $response->assertSessionHas('status', 'verification-link-sent');
    }

    public function test_protected_route_requires_authentication(): void
    {
        $response = $this->get('/dashboard');

        $response->assertRedirect('/login');
    }
}

Pest ​

php
<?php

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

test('users can authenticate', function () {
    $user = User::factory()->create([
        'email' => 'user@example.com',
        'password' => bcrypt('password'),
    ]);

    $response = $this->post('/login', [
        'email' => 'user@example.com',
        'password' => 'password',
    ]);

    $this->assertAuthenticated();
    $response->assertRedirect('/dashboard');
});

test('users cannot authenticate with invalid password', function () {
    $user = User::factory()->create([
        'email' => 'user@example.com',
        'password' => bcrypt('password'),
    ]);

    $this->post('/login', [
        'email' => 'user@example.com',
        'password' => 'wrong-password',
    ]);

    $this->assertGuest();
});

test('users can register', function () {
    $response = $this->post('/register', [
        'name' => 'Test User',
        'email' => 'test@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    $this->assertAuthenticated();
    $response->assertRedirect('/dashboard');
});

test('unauthenticated user is redirected to login', function () {
    $response = $this->get('/dashboard');

    $response->assertRedirect('/login');
});

Cheatsheet Fortify ​

📦 Instalasi ​

bash
composer require laravel/fortify
php artisan fortify:install
php artisan migrate

⚙️ Features (di config/fortify.php) ​

php
'features' => [
    Features::registration(),
    Features::resetPasswords(),
    Features::emailVerification(),
    Features::twoFactorAuthentication(['confirm' => true, 'confirmPassword' => true]),
    Features::passkeys(['confirmPassword' => true]),
],

🎨 View Customization (di FortifyServiceProvider) ​

MethodViewRoute
Fortify::loginView()auth.loginGET /login
Fortify::registerView()auth.registerGET /register
Fortify::requestPasswordResetLinkView()auth.forgot-passwordGET /forgot-password
Fortify::resetPasswordView()auth.reset-passwordGET /reset-password/{token}
Fortify::verifyEmailView()auth.verify-emailGET /email/verify
Fortify::confirmPasswordView()auth.confirm-passwordGET /user/confirm-password
Fortify::twoFactorChallengeView()auth.two-factor-challengeGET /two-factor-challenge

🧩 Custom Actions (app/Actions/Fortify/) ​

ClassFungsi
CreateNewUserValidasi & buat user baru
ResetUserPasswordValidasi & update password
PasswordValidationRulesAturan validasi password
Fortify::authenticateUsing()Custom logic autentikasi

🛡️ 2FA Endpoints ​

MethodRouteFungsi
POST/user/two-factor-authenticationEnable 2FA
DELETE/user/two-factor-authenticationDisable 2FA
POST/user/confirmed-two-factor-authenticationConfirm 2FA
GET/user/two-factor-qr-codeQR code SVG
GET/user/two-factor-recovery-codesRecovery codes
POST/user/two-factor-recovery-codesRegenerate codes

🔑 Passkey Endpoints ​

MethodRouteFungsi
GET/passkeys/login/optionsChallenge untuk login
POST/passkeys/loginLogin dengan passkey
POST/user/passkeysRegister passkey baru
DELETE/user/passkeys/{passkey}Hapus passkey

🧪 Testing ​

php
// Auth user
$this->actingAs($user);

// Cek authenticated
$this->assertAuthenticated();
$this->assertGuest();

// Test register
$this->post('/register', [
    'name' => 'Test',
    'email' => 'test@example.com',
    'password' => 'password',
    'password_confirmation' => 'password',
])->assertRedirect('/dashboard');

⚙️ Konfigurasi Penting ​

FileKonfigurasi
config/fortify.phpfeatures, home, views, limiters
config/auth.phpGuard web untuk session
.envAPP_URL (untuk passkey relying_party_id)
app/Providers/FortifyServiceProvider.phpView, actions, pipeline

Panduan Konseptual: Untuk penjelasan tentang apa itu Fortify, perbandingan dengan Sanctum, dan kapan menggunakannya, lihat file auth-dan-laravel.md.