fissible / vouch
Laravel authentication with password, OTP, MFA, and recorded session assurance.
Requires
- php: ^8.4
- giggsey/libphonenumber-for-php-lite: ^9.0
- illuminate/console: ^13.0
- illuminate/database: ^13.0
- illuminate/hashing: ^13.0
- illuminate/queue: ^13.0
- illuminate/support: ^13.0
- psr/clock: ^1.0
- spomky-labs/otphp: ^11.5
- symfony/string: ^7.4.6 || ^8.0.6
Requires (Dev)
- cweagans/composer-patches: ^1.7
- larastan/larastan: ^3.10
- orchestra/testbench: ^11.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-arch: ^3.0
- pestphp/pest-plugin-mutate: 3.0.5
- phpstan/phpstan: ^2.0
- silber/bouncer: ^1.0
- spatie/laravel-permission: ^8.3
Suggests
- spatie/laravel-permission: Use it for roles and permissions; Vouch handles authentication and session assurance.
This package is auto-updated.
Last update: 2026-08-29 19:26:00 UTC
README
Vouch — authenticates your users to the degree you need. What they may do stays yours.
Vouch is Laravel authentication with password, OTP, MFA, and recorded session assurance behind one policy engine. It orchestrates authentication factors and step-up; it does not reimplement their cryptography or protocols.
Vouch is not an authorization package, token storage, or a UI. Your application or its authorization package decides who may act; Vouch records how strongly that person authenticated and can require a stronger session for that action. Token issuance and token assurance are planned for 2.4, and presentation remains the host application's responsibility. OIDC and federation are not planned.
Requirements and maturity
Vouch requires PHP ^8.4 and Laravel 13 components (illuminate/* ^13.0). It
is pre-1.0 software: account lifecycle and assurance work ships in Phase 2.3,
the token-issuance gate is planned for 2.4, and standard UI adapters are Phase
3 work. See the roadmap for those phases.
Its account-lifecycle services cover identifier verification, credential recovery, first-credential enrollment, and credential self-service. The host still supplies presentation and application policy.
Install and adopt
Install the package:
composer require fissible/vouch
Publish the configuration and migrations, then configure the package before enabling login:
php artisan vendor:publish --tag=vouch-config php artisan vendor:publish --tag=vouch-migrations php artisan migrate php artisan vouch:doctor
Before adding an assurance map or a direct assurance route, set
VOUCH_STEP_UP_URL to the host's routeable step-up presentation. Vouch does
not ship a step-up page. If an interactive request is refused and this value is
unset, Vouch deliberately throws a RuntimeException (a 500 response) rather
than redirecting a browser to its POST-only endpoint.
vouch:doctor checks adoption readiness as aggregate state; it never accepts
an identifier. Before login is live, verify identifier control through
IdentifierVerifier, bind real OtpDelivery and DeliveryEconomics
implementations, and run a durable asynchronous queue worker for
vouch.otp.queue. If CAPTCHA escalation is enabled, bind CaptchaVerifier as
well. The full prerequisite staircase, queue operation, and maintenance commands
are in the operations guide.
Compose authorization with assurance
Vouch deliberately leaves authorization to the host. For a host using
spatie/laravel-permission, the authorization middleware decides whether the
user has invoices.approve; Vouch's ability map says that this same ability
needs an aal2 session. The route uses the host package's permission rule; the
map is central, so the assurance requirement is not repeated on each route.
Laravel 11+ hosts register Spatie's middleware alias themselves, for example in
bootstrap/app.php:
use Illuminate\Foundation\Configuration\Middleware; ->withMiddleware(function (Middleware $middleware): void { $middleware->alias([ 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, ]); })
use App\Http\Controllers\ApproveInvoiceController; use Illuminate\Support\Facades\Route; Route::post('/invoices/{invoice}/approve', ApproveInvoiceController::class) ->middleware(['permission:invoices.approve']);
Spatie also ships a static helper that skips the alias entirely, which is worth
knowing if you would rather not claim the generic name permission in your
application's alias table:
use Spatie\Permission\Middleware\PermissionMiddleware; Route::post('/invoices/{invoice}/approve', ApproveInvoiceController::class) ->middleware([PermissionMiddleware::using('invoices.approve')]);
Vouch reads either form: the ability names come from the middleware parameters on the matched route, and the alias is resolved through the router's own table.
// Published Vouch configuration 'assurance_requirements' => [ 'invoices.approve' => 'aal2', ],
The package's vouch.ability middleware reads the authorization declarations
on the matched route and applies the strongest mapped requirement. It only
refuses or sends a browser to the configured vouch.step_up.presentation_url;
it never grants permission. This means you can use the same map with plain
Laravel Gates, Spatie permission middleware, or another authorization system
that exposes ability names on routes.
Enforcement boundary
Vouch adds vouch.ability to the web and api middleware groups only. A
protected route in another or custom group must add vouch.ability explicitly,
or it is not covered by route enforcement. Run php artisan vouch:assurance-map
and inspect its enforced_groups field after configuring middleware, rather
than assuming a host's group is protected.
For a direct requirement on one route or group that is not derived from an
ability map, use the vouch.assurance alias, for example
->middleware('vouch.assurance:aal2'). It has the same presentation-URL
prerequisite as the map middleware.
The Gate hook is defense in depth, not the enforcement point. An earlier
Gate hook can grant an ability and bypass a later hook, so only the route
middleware can enforce the mapped assurance before that grant short-circuits
the check. The measured package-specific paths and their limits are documented
in the authorization integration survey.
For an authenticated request with no Vouch session, a mapped route returns a
403 response with insufficient_assurance; it does not evaluate a bearer token
or fail open. That remains the boundary until 2.4 adds token assurance.
Strict maps
Set vouch.assurance_strict only after listing the host's intentional ability
vocabulary in vouch.declared_abilities. Strict mode cannot use
Gate::abilities() for that list: it is empty at boot for abilities defined
only at runtime, and it does not enumerate policy methods or database-backed
permissions. A host that defines an ability solely with Gate::define must
still list it in vouch.declared_abilities when strict mode is enabled.
Host integration blind spots
php artisan vouch:assurance-map --json reports
user_model_routes_to_gate. Treat a false result as a coverage warning: a
model using Bouncer's Authorizable trait can silently take over can() and
stop that method reaching the Gate, without an error. The report can expose
that model seam, but it cannot detect Bouncer::runBeforePolicies() at all.
That switch moves Bouncer's grant into its before slot, where it can bypass a
later deny-only hook; keep the route middleware as the enforcement boundary.
See operations for adoption and runtime operation, and the authorization integration survey for the measured Spatie, Bouncer, and Gate behavior behind these limits.