laravel-rebel-email-otp
GitHub repository · Composer: padosoft/laravel-rebel-email-otp · MIT
Passwordless login that holds up in production. A one-time code by email, with anti-enumeration baked in, rate limiting across four dimensions, and a Sanctum token pair for your mobile clients — every step recorded in the audit trail.
Email-OTP is an AAL1 factor (NIST 800-63B). It is great for low-friction login, but it is not phishing-resistant — pair it with passkeys and step-up for sensitive actions.
What it is
laravel-rebel-email-otp delivers passwordless login: a user enters their email, receives a numeric one-time code, and verifies it. On success it returns a web login result or a Sanctum TokenPair (access + refresh) for API and mobile clients. The whole flow is tenant-, purpose- and risk-aware and emits audit events through the core vocabulary.
The problem it solves
Rolling your own email-OTP looks simple until you hit the hard parts: an attacker can probe which addresses exist (account enumeration), a single rate limit is trivially bypassed, and OTPs end up in logs. This package closes those gaps by design — the start endpoint always returns the same response whether or not the address exists, rate limiting is enforced across IP, identifier, tenant and purpose, and the code never reaches the audit log in cleartext.
What you get
- Passwordless email-OTP for web and mobile, with a Sanctum
TokenPairfor API clients. - Anti-enumeration: the same response is returned regardless of whether the identifier exists.
- Multi-dimensional rate limiting: IP + identifier + tenant + purpose.
- Multi-tenant / multi-purpose / risk-aware challenges out of the box.
- Audited outcomes: verified logins are recorded as AAL1 with AMR
['otp', 'email']. - Lifecycle hygiene: a console command prunes expired challenges.
When to use it
- You want passwordless login for a web app, a mobile app, or both.
- You need a login flow that is safe against account enumeration by default.
- You serve multiple tenants or purposes and need challenges scoped per dimension.
- You want login outcomes in the audit trail without wiring it yourself.
Worked example
composer require padosoft/laravel-rebel-email-otp
php artisan vendor:publish
php artisan migrate
The package registers its web routes (routes/web.php) and an EmailOtpController that drives the start / resend / verify actions, plus a console command (PruneChallengesCommand) you can schedule to clear expired challenges.
A verified challenge emits an audit event at AAL1 with AMR ['otp', 'email'] — so an admin or risk engine can later tell that this session was established by email-OTP and not by a stronger factor.
How it fits
This package builds directly on padosoft/laravel-rebel-core: it issues the Sanctum TokenPair, stores identifiers as keyed HMACs, and records verifications through the core audit trail. Because the verified factor carries its assurance level (AAL1, non-phishing-resistant), step-up can later require a stronger factor before a sensitive action proceeds.
A purpose-built, audited, anti-enumeration OTP flow beats a hand-rolled one — see Why Rebel.
Reference
Runtime files
src\Actions\ResendEmailOtpChallenge.phpsrc\Actions\StartEmailOtpChallenge.phpsrc\Actions\VerifyEmailOtpChallenge.phpsrc\Console\PruneChallengesCommand.phpsrc\Enums\ChallengeStatus.phpsrc\Http\Controllers\EmailOtpController.phpsrc\Models\EmailOtpChallenge.phpsrc\Notifications\EmailOtpNotification.phpsrc\Otp\NumericOtpGenerator.phpsrc\Otp\OtpHasher.phpsrc\Resolvers\NullSubjectResolver.phpsrc\Results\StartEmailOtpResult.phpsrc\Results\VerifyEmailOtpResult.phpsrc\RebelEmailOtp.phpsrc\RebelEmailOtpServiceProvider.php
Service providers
src\RebelEmailOtpServiceProvider.php
Services and managers
src\Resolvers\NullSubjectResolver.phpsrc\RebelEmailOtpServiceProvider.php
Contracts
None detected in the package tree.
Controllers
src\Http\Controllers\EmailOtpController.php
Middleware
None detected in the package tree.
Models
src\Models\EmailOtpChallenge.php
Config
config\rebel-email-otp.php
Migrations
database\migrations\create_rebel_email_otp_challenges_table.php
Routes
routes\web.php
Commands
src\Console\PruneChallengesCommand.php
Composer requirements
| Dependency | Constraint |
|---|---|
illuminate/contracts |
`^12.0 |
illuminate/support |
`^12.0 |
padosoft/laravel-rebel-core |
^0.1 |
php |
^8.3 |
spatie/laravel-package-tools |
^1.92 |
Development requirements
| Dependency | Constraint |
|---|---|
larastan/larastan |
^3.0 |
laravel/pint |
^1.18 |
orchestra/testbench |
`^10.0 |
pestphp/pest |
^4.0 |
pestphp/pest-plugin-laravel |
^4.0 |
ADR
Problem: keep laravel-rebel-email-otp replaceable
Decision: document its public responsibility and use Rebel core contracts at integration boundaries.
Consequences: applications can adopt the package without coupling every other Rebel module to its internals.
Problem: package-specific behavior must remain auditable
Decision: all security-significant outcomes should emit or feed audit events through the core vocabulary.
Consequences: admin API, admin UI and AI guard can reason across packages without bespoke parsers for every provider.
Test & verification surface
tests\Feature\EmailOtpFlowTest.phptests\Feature\EmailOtpResendTest.phptests\Feature\EmailOtpSubjectTest.phptests\Feature\EmailOtpTenantTest.phptests\Feature\EmailOtpWebFlowTest.phptests\Feature\MigrationTest.phptests\Feature\PruneChallengesTest.phptests\Unit\NumericOtpGeneratorTest.phptests\Unit\SkeletonTest.phptests\Pest.phptests\TestCase.php
Do not copy internal test-only classes into an application. Treat file lists as a source map for maintainers and auditors, not as an installation recipe by themselves.