padosoft/laravel-rebel-bridge-fortify

Bridge between Laravel Fortify and Laravel Rebel: exposes password-confirm / passkey / TOTP as step-up drivers, maps Fortify events into the Rebel audit trail, and enables passkey-first login. Part of padosoft/laravel-rebel-*.

Maintainers

πŸ‘ lopadova

Package info

github.com/padosoft/laravel-rebel-bridge-fortify

pkg:composer/padosoft/laravel-rebel-bridge-fortify

Statistics

Installs: 87

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 1

v0.1.1 2026-06-03 17:58 UTC

Suggests

  • laravel/fortify: Enables the TOTP step-up driver and the Fortify two-factor event mapping. Not required for password-confirm (framework hasher) or passkey (needs a PasskeyConfirmer binding).
  • padosoft/laravel-rebel-email-otp: Enables the email-OTP fallback for passkey-first login.

Provides

None

Conflicts

None

Replaces

None

MIT d5007f70f4d4136b7822f8df78e781fbb947ad59

AuthenticationlaraveltotppadosoftfortifyRebelpasskeystep-up

This package is auto-updated.

Last update: 2026-06-20 11:47:05 UTC


README

Official documentation: https://doc.laravel-rebel.padosoft.com

Make Laravel Fortify's factors first-class step-up methods. This bridge turns Fortify's password confirmation, TOTP two-factor, and passkeys into Rebel step-up drivers β€” so you can require the right strength of re-authentication per sensitive action β€” and it folds Fortify's login/2FA events into one unified Rebel audit trail. It also ships a passkey-first login flow with an email-OTP fallback. Part of the padosoft/laravel-rebel-* suite.

πŸ‘ Laravel Rebel

πŸ‘ Laravel 12|13
πŸ‘ PHP 8.3+
πŸ‘ PHPStan max
πŸ‘ Pest 4
πŸ‘ Fortify bridge
πŸ‘ MIT

Table of contents

What it is (and what it is not)

It is the glue between Laravel Fortify and the Rebel step-up engine (padosoft/laravel-rebel-step-up). Fortify gives your app password/2FA/passkey plumbing; Rebel gives you a policy layer ("this action needs AAL2 + phishing-resistant"). This bridge lets the two talk: Fortify's factors become Rebel step-up drivers, and Fortify's auth events become Rebel audit records.

It is not a login UI and it does not replace Fortify β€” keep using Fortify for registration, login, 2FA enrolment and passkey management. This package adds the re-authentication policy and audit unification on top.

Depends on padosoft/laravel-rebel-core and padosoft/laravel-rebel-step-up. Fortify itself is optional (feature-detected): without it, the password-confirm driver still works and the Fortify-only pieces are simply skipped.

Quick glossary (one minute)

Term In plain words
Step-up "You're already logged in, but for THIS action prove it's really you again."
Step-up driver A way to perform that proof: password, TOTP, passkey… Each declares the strength it provides.
AAL (Authenticator Assurance Level) NIST strength level. aal1 = one factor (e.g. a password); aal2 = two factors / stronger.
AMR Authentication Methods References β€” the methods used, e.g. ['pwd'], ['otp','totp'], ['webauthn'].
Phishing-resistant A proof phishing can't steal β€” typically a passkey/FIDO2. A password or a TOTP is not.
TOTP The 6-digit code from an authenticator app (Google Authenticator, 1Password…).
Recovery code A one-time backup code used when the authenticator device is unavailable.
Audit trail One table (rebel_auth_events) where every auth event lands, regardless of which library produced it.

Why this bridge β€” the moats

β˜… What In short
β˜…β˜…β˜… Per-action strength Require aal2 + phishing-resistant for a payout, just a password for a profile tweak β€” declaratively, via step-up policies.
β˜…β˜…β˜… NIST-correct assurance Password = AAL1, TOTP = AAL2, passkey = AAL2 phishing-resistant. No over-claiming: a password can't satisfy a high-assurance action.
β˜…β˜…β˜… Replay-resistant by design Passkey confirmations are bound to a single-use server challenge; recovery codes are consumed atomically (row-locked).
β˜…β˜… Unified audit Fortify logins, failures, lockouts and 2FA events all land in rebel_auth_events β€” lockouts with HMAC'd IP/identifier (no plaintext PII).
β˜…β˜… Passkey-first login Offer passkeys first, fall back to email-OTP automatically when the user has none.
β˜…β˜… Optional Fortify Feature-detected: installs and partly works even without Fortify; no hard crash.
β˜… Pluggable passkeys Bring your own WebAuthn implementation via a tiny contract; a fake ships for tests.

Rebel Fortify Bridge vs the alternatives

How "re-authenticate before a sensitive action" looks with each approach:

Capability Rebel Fortify Bridge Shopify Fortify alone Laravel password.confirm Hand-rolled
Re-confirm with a password βœ… βž– βœ… βœ… βœ…
Re-confirm with TOTP βœ… ❌ ❌ ❌ ❌
Re-confirm with a passkey βœ… ❌ ❌ ❌ ❌
Per-action required strength (AAL/AMR) βœ… ❌ ❌ ❌ ❌
Rejects a factor below the required assurance βœ… ❌ ❌ ❌ ❌
Passkey confirm bound to a single-use challenge βœ… ❌ βž– ❌ ❌
Atomic, single-use recovery codes for step-up βœ… ❌ βž– (login only) ❌ ❌
Confirmation decays after a TTL βœ… βž– βž– βœ… ❌
PSD2/SCA dynamic linking (amount+payee) βœ… (via step-up) ❌ ❌ ❌ ❌
Unified audit trail across login + 2FA + step-up βœ… βž– ❌ ❌ ❌
Lockout audit with HMAC'd IP/identifier βœ… ❌ ❌ ❌ ❌
Multi-tenant aware βœ… ❌ ❌ ❌ ❌

Legend: βœ… built-in Β· βž– partial / only in a narrow flow / hosted-only / not exposed to you Β· ❌ not available. Fortify is excellent at what it does (login, 2FA enrolment, passkey management) β€” this bridge builds the policy + audit layer on top of it, it does not compete with it. Note on Shopify: it is a hosted, closed commerce platform you can neither self-host nor extend β€” it exposes none of these re-authentication primitives to your own Laravel app, so it's a black box you don't control.

How it works (step by step)

You configure a step-up "purpose" (in laravel-rebel-step-up) and list the drivers it accepts:

 'checkout-credit-order' => [
 'required_assurance' => 'aal2',
 'require_phishing_resistant' => true,
 'drivers' => ['fortify_passkey_confirm', 'fortify_totp'], // <- from THIS bridge
 ]
 |
 v
This bridge registers fortify_password_confirm / fortify_totp / fortify_passkey_confirm
into the step-up DriverRegistry at boot (each only if it can actually work).
 |
 v
When the user hits a protected action, the step-up engine picks the best allowed driver:
 - passkey available? -> issue a challenge, verify the assertion (phishing-resistant, AAL2)
 - else TOTP? -> verify the 6-digit code or a recovery code (AAL2)
 - else password? -> verify the password (AAL1) [only if the policy allows AAL1]
 |
 v
Meanwhile, every Fortify/framework auth event (login, failure, lockout, 2FA enabled...)
is mapped into rebel_auth_events -- one audit trail for the whole stack.

Installation (junior-proof)

Prerequisites: Laravel 12 or 13, PHP 8.3+, and padosoft/laravel-rebel-core + padosoft/laravel-rebel-step-up installed (they come as dependencies). Laravel Fortify is recommended but optional.

1) Require the package

composer require padosoft/laravel-rebel-bridge-fortify

2) (Recommended) install Fortify and enable two-factor / passkeys

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

Enable the features you want in config/fortify.php (e.g. Features::twoFactorAuthentication()). See the Fortify docs for the enrolment UI.

3) Publish the bridge config (optional)

php artisan vendor:publish --tag="rebel-bridge-fortify-config"

4) Use the drivers in your step-up policies (config/rebel-step-up.php):

'purposes' => [
 'change-email' => [
 'required_assurance' => 'aal1',
 'drivers' => ['fortify_password_confirm'],
 ],
 'checkout-credit-order' => [
 'required_assurance' => 'aal2',
 'require_phishing_resistant' => true,
 'drivers' => ['fortify_passkey_confirm', 'fortify_totp'],
 ],
],

That's it β€” the bridge has already registered the drivers; the step-up engine will use them.

Configuration (every option)

File config/rebel-bridge-fortify.php:

Key Default What it does
drivers.password_confirm true Register the fortify_password_confirm step-up driver (works even without Fortify).
drivers.totp true Register fortify_totp β€” only when Laravel Fortify is installed.
drivers.passkey true Register fortify_passkey_confirm β€” only when a PasskeyConfirmer is bound (see below).
audit_events true Map framework + Fortify auth events into rebel_auth_events.

To enable the passkey driver, bind your WebAuthn implementation:

// In a service provider
use Padosoft\Rebel\Bridge\Fortify\Contracts\PasskeyConfirmer;

$this->app->singleton(PasskeyConfirmer::class, MyWebAuthnPasskeyConfirmer::class);

(For passkey-first login you bind PasskeyAuthenticator the same way.)

Usage examples

1. Require a strong step-up for a sensitive action

Protect a route with the step-up middleware (from laravel-rebel-step-up); this bridge supplies the factors the policy is allowed to use:

Route::middleware(['auth', 'rebel.stepup:checkout-credit-order'])
 ->post('/checkout/confirm', [CheckoutController::class, 'confirm']);

With the policy above, the engine will demand a passkey (or TOTP) β€” a password alone won't pass, because it's only AAL1.

2. Password re-confirmation (sudo mode)

// config/rebel-step-up.php
'delete-account' => [
 'required_assurance' => 'aal1',
 'drivers' => ['fortify_password_confirm'],
],
// The user re-enters their password; the driver verifies it via the framework hasher.
$result = app(\Padosoft\Rebel\StepUp\RebelStepUp::class)
 ->confirm($challengeId, $request->string('password'), $ctx);

3. TOTP and recovery codes

When the policy lists fortify_totp, the user submits their 6-digit code β€” or, if they lost their device, a recovery code (consumed once, atomically):

$result = $stepUp->confirm($challengeId, $request->string('code'), $ctx);
// '123456' -> verified via the authenticator app
// 'ABCD-EFGH-...' -> verified via a recovery code (then invalidated)

4. Passkey step-up (phishing-resistant)

// 1) start() issues a single-use challenge -- send it to the browser
$start = $stepUp->start($ctx); // $start->reference = the WebAuthn challenge

// 2) the browser produces an assertion via navigator.credentials.get(); verify it
$result = $stepUp->confirm($start->challengeId, $assertionJson, $ctx);

The assertion is verified against that challenge, so a captured assertion cannot be replayed.

5. Passkey-first login with email-OTP fallback

use Padosoft\Rebel\Bridge\Fortify\PasskeyFirstLogin;

public function begin(Request $request, PasskeyFirstLogin $login)
{
 $options = $login->begin($request->string('email'));

 if ($options === null) {
 // No passkey for this user -> fall back to email-OTP (laravel-rebel-email-otp)
 return response()->json(['fallback' => 'email_otp']);
 }

 // Persist the challenge (e.g. in the session) to bind it on completion
 $request->session()->put('passkey_challenge', $options['challenge']);

 return response()->json(['passkey' => $options]);
}

public function complete(Request $request, PasskeyFirstLogin $login)
{
 $user = $login->complete(
 $request->string('assertion'),
 (string) $request->session()->pull('passkey_challenge'),
 );

 return $user !== null
 ? response()->json(['ok' => true])
 : response()->json(['error' => 'invalid'], 422);
}

6. Unified audit trail

No code needed β€” once installed, framework and Fortify events are recorded automatically:

use Padosoft\Rebel\Core\Models\RebelAuthEvent;

RebelAuthEvent::query()->where('event_type', 'login.succeeded')->latest()->take(20)->get();
RebelAuthEvent::query()->where('event_type', 'login.lockout')->get(); // IP/identifier are HMAC'd
RebelAuthEvent::query()->where('event_type', 'fortify.two_factor.enabled')->get();

Rich login context (since 0.1.1). Framework login/logout events are recorded with the request IP and User-Agent (keyed HMACs, never plaintext) and a successful password login records aal = aal1 plus the pwd factor in amr β€” so the admin panel's Audit Explorer shows IP, country (via laravel-rebel-core's geo capture), assurance and AMR for these events, not blanks.

The assurance hierarchy (important)

This bridge is deliberately honest about strength, so a weak factor can never satisfy a strong requirement:

Driver AAL Phishing-resistant Good for
fortify_password_confirm AAL1 ❌ Low-risk re-auth ("sudo mode"), profile edits.
fortify_totp AAL2 ❌ Medium-risk actions; the everyday second factor.
fortify_passkey_confirm AAL2 βœ… High-value actions (payments, credit orders, recovery).

A purpose that requires aal2 + require_phishing_resistant can only be satisfied by a passkey. rebel:validate-config (from the step-up package) fails your CI if a purpose lists no driver that can meet its bar.

.env.example

# Which Fortify-backed step-up drivers to register
REBEL_FORTIFY_DRIVER_PASSWORD=true
REBEL_FORTIFY_DRIVER_TOTP=true
REBEL_FORTIFY_DRIVER_PASSKEY=true

# Map framework + Fortify auth events into the Rebel audit trail
REBEL_FORTIFY_AUDIT_EVENTS=true

Security notes

  • No assurance over-claiming: assurance levels follow NIST (password = AAL1, TOTP = AAL2, passkey = AAL2 phishing-resistant). The step-up engine enforces them against the policy.
  • Passkey replay resistance: confirmations are bound to a single-use, server-issued challenge; a missing challenge is refused.
  • TOTP replay: delegated to Fortify's TwoFactorAuthenticationProvider (cache-backed in a real Fortify install), plus every step-up challenge is single-use.
  • Recovery codes: consumed atomically (row lock + targeted update) so they can't be redeemed twice, and the consumption is audited (fortify.recovery_code.used).
  • No plaintext PII in audit: lockouts store IP and identifier as keyed HMACs; a failed passkey login does not claim a WebAuthn AMR.

πŸ”‹ Vibe coding with batteries included

This package ships AI batteries β€” so you (and your AI agent) can extend it correctly on the first try:

  • CLAUDE.md β€” a concise AI working guide (purpose, conventions, architecture, how to extend, Definition of Done). Plain Markdown, so Claude Code, Cursor, Copilot and Codex all read it.
  • AGENTS.md β€” the agent/workflow contract (branch β†’ PR β†’ CI β†’ tag/release, the gates).
  • .claude/skills/ β€” invocable skills (at least rebel-package-dev) encoding the suite's TDD loop, the PHPStan-level-max recipes, the security/telemetry rules, and the release discipline.

Open the repo in your AI editor and just start β€” the rules, guardrails and extension recipes come with it. PRs that follow the shipped CLAUDE.md pass CI (PHPStan max + Pest + Pint) and review the first time around.

Testing & License

composer test # Pest (drivers, event mapping, passkey-first login, step-up integration)
composer phpstan # static analysis, level max
composer pint # code style

License: MIT β€” see LICENSE. Part of the padosoft/laravel-rebel suite.