rainwaves / lara-auth-suite
Laravel API auth suite: Sanctum tokens & session modes, password reset, and 2FA.
Requires
- php: ^8.2|^8.3|^8.4
- laravel/framework: ^11.0|^12.0|^13.0
Requires (Dev)
- doctrine/annotations: ^2.0
- larastan/larastan: ^3.10
- laravel/pint: ^1.25
- laravel/sanctum: ^4.0|^5.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0
- zircote/swagger-php: ^4.10
Suggests
- laravel/sanctum: Required if authx.mode includes token authentication (checked at boot; see LaraAuthSuiteServiceProvider::verifyRuntimeDependencies).
- spatie/laravel-permission: Required if authx.permissions.enabled is true and roles/permissions are actually assigned (checked when a real assignment is attempted; see SpatiePermissionSyncService).
This package is auto-updated.
Last update: 2026-08-17 08:03:38 UTC
README
Modern, flexible authentication for Laravel APIs & SPAs.
Plug-and-play authentication for Laravel 10/11/12/13, supporting both API token auth (Sanctum) and session-based auth for SPAs — with password resets, backend-enforced Two-Factor Authentication (Email OTP + TOTP), recovery codes, and full role/permission support.
Overview
Rainwaves/Lara Auth Suite gives you full authentication without writing boilerplate:
- Token authentication for mobile apps or external APIs
- Session authentication for SPAs (Vue / React / Inertia / Livewire)
- Unified password reset flow
- Two-Factor Authentication (Email OTP, Authenticator App)
- TOTP replay-attack protection
- Recovery codes for locked-out users
- Automatic role & permission assignment (Spatie Permissions)
- Feature-flag gating — enable only what you need
Ideal for:
- SaaS platforms
- Admin dashboards
- Multi-tenant SPAs
- Hybrid apps needing both tokens + sessions
Demo Applications
Backend (Laravel)
Reference backend implementation using the package:
https://github.com/Magnificent-Big-J/lara-auth-suite-demo
Frontend (Nuxt SPA)
Full SPA login + 2FA flow:
https://github.com/Magnificent-Big-J/lara-auth-suite-nuxt-demo
Features
| Feature | Status | Description |
|---|---|---|
| Sanctum PAT login | Done | Token-based API authentication |
| Session authentication | Done | Laravel guard + CSRF protection |
| Password reset (email) | Done | Full reset flow with throttle |
| 2FA: Email OTP | Done | Secure email verification codes |
| 2FA: TOTP (Authenticator App) | Done | Google Authenticator / Authy / 1Password |
| 2FA enforcement | Done | Enforced on protected routes (backend-driven) |
| TOTP replay protection | Done | last_used_counter prevents counter reuse |
| Recovery codes | Done | 8-code offline fallback for locked-out TOTP users |
| Security throttles | Done | Login/reset/2FA rate limiting |
| Pending auth manager | Done | Purpose-bound (login-challenge vs. MFA-setup) session-less state bridge |
| Feature flag gating | Done | Disable unused auth features at the route level |
| Mandatory MFA setup | Done | enforcement: required routes unenrolled users into setup, never silent access |
| Password-reset revocation | Done | Reset revokes tokens, invalidates sessions, invalidates pending 2FA contexts |
| Recent-password confirmation | Done | TTL-bound window so sensitive 2FA actions don't need password on every request |
| Independent rate limits | Done | Separate per-account and per-IP buckets, can't be bypassed by rotating either |
| Finite, scoped tokens | Done | 7-day expiry and auth:read-only abilities by default |
| Security event coverage | Done | Full event set with enforced payload redaction (no secrets ever leak) |
| Config validator | Done | php artisan authx:check [--production] |
| Trusted devices | Planned | Device remembering |
| Token/session/device mgmt | Planned | Revoke, audit |
Requirements
| Laravel | PHP | Sanctum |
|---|---|---|
| 11 / 12 / 13 | 8.2 / 8.3 / 8.4 | 4.x / 5.x |
Sanctum is required only when token mode is enabled (authx.mode: token or both) — session-only installs don't need it. The package checks this at boot and fails clearly if token mode is on without Sanctum installed.
Breaking Changes in v2.1.0
Read this section carefully before upgrading from v2.0.x. See CHANGELOG.md for the full list; the essentials:
ITwoFactorRequirement::shouldRequire(): bool is now decide(): TwoFactorRequirementResult.
The boolean couldn't represent "no factor enrolled under mandatory 2FA" as anything other than "not required" — a real MFA bypass under enforcement: required. Switch to decide() and branch on isRequired() / requiresSetup() / requiresChallenge().
PendingAuthManager::startSession()/startToken() require a PendingAuthPurpose argument.
Pass PendingAuthPurpose::LoginChallenge or PendingAuthPurpose::MfaSetup. Pending contexts are now purpose-bound and reject use for the wrong purpose.
Token defaults changed. tokens.default_abilities was ['*'], now ['auth:read']. tokens.expiry_minutes was null (never expires), now 10080 (7 days). Set both back explicitly if you need the old behavior.
authx.throttle config keys renamed and split into independent per-account/per-IP pairs (e.g. login_per_account/login_per_ip). The old combined keys (login, two_factor, reset) are no longer read.
2fa.require_password_on_enable now defaults to true.
Re-publish config/authx.php (or diff your published copy) — an app-published config silently overrides the new safe defaults if left stale on the v2.0 shape.
No new migrations in v2.1.0.
Breaking Changes in v2.0.0
Read this section carefully before upgrading from v1.x.
Registration is disabled by default.
registration.enabled now defaults to false. If you have not published authx.php, registration will be silently disabled after upgrading. Publish (or re-publish) the config and set enabled: true explicitly.
issue_token_on_register defaults to false.
Token issuance on registration is now opt-in. Existing apps that relied on the true default must set this explicitly in their published config.
sendEmailOtp() no longer returns the OTP.
The return type changed from string to void. The plaintext code must never escape the service layer. Any code that called sendEmailOtp() and used the return value must be updated.
ITokenAuthService interface type hint corrected.
The interface previously imported Illuminate\Auth\Authenticatable (a trait). It now imports Illuminate\Contracts\Auth\Authenticatable (an interface). Custom implementations that bound to the old type hint must update their use statement.
Session is not started until 2FA passes. When 2FA is required, the session is now invalidated and the CSRF token regenerated before the challenge is issued. The session is only created after successful verification. Frontends that assumed a valid session existed during the challenge step must be updated.
SessionLoginResult::$channel is now a TwoFactorChannel enum.
The field was previously a plain string. Code that compares the value using string literals must use the enum instead, e.g. TwoFactorChannel::Email.
New migration required.
Run php artisan migrate after upgrading. The new migration adds last_used_counter to the two_factor_secrets table for TOTP replay protection.
Installation
composer require rainwaves/lara-auth-suite
Publish configuration:
php artisan vendor:publish \
--provider="Rainwaves\\LaraAuthSuite\\LaraAuthSuiteServiceProvider" \
--tag=authx-config
Run migrations:
php artisan migrate
Authentication Flow
Authentication decisions are enforced on the backend. Frontend clients do not decide authentication state.
- Credentials are validated
- If 2FA is required: the session is invalidated, a pending auth state is created, and an OTP/TOTP challenge is issued — the user is not logged in yet
- After successful 2FA: the session is created, CSRF token is issued, and the user is fully authenticated
- If 2FA is not required: the session is created and the user is fully authenticated immediately
This prevents:
- Logged-in-but-unverified states
- Session persistence before verification
- Frontend-controlled security decisions
- Password reset user enumeration
- TOTP replay attacks
Usage
1. Login (Session Mode – SPA)
POST /auth/session/login
{
"email": "admin@example.com",
"password": "secret",
"remember": true
}
Response (2FA required — session not yet active):
{
"user": {},
"requires_two_factor": true,
"channel": "email"
}
Response (2FA not required):
{
"user": {},
"requires_two_factor": false
}
2. Login (Token Mode / API Clients)
POST /auth/login
{
"email": "admin@example.com",
"password": "secret"
}
Response (no 2FA):
{
"token": "plain-text-token",
"token_type": "Bearer",
"abilities": ["auth:read"],
"expires_at": "2026-08-04T12:00:00Z"
}
Response (2FA required — token is null until verified):
{
"token": null,
"requires_two_factor": true,
"channel": "totp",
"pending_auth_id": "uuid"
}
Response (enforcement: required and no factor enrolled yet — token is null, no OTP is sent because nothing is enrolled):
{
"token": null,
"status": "two_factor_setup_required",
"pending_auth_id": "uuid"
}
Complete setup with the pending_auth_id from this response — it carries an mfa_setup-purpose pending context, so POST /auth/2fa/totp/enable is reachable without a bearer token for this specific flow. Once TOTP is verified, a normal token is issued.
3. Get Current User
GET /auth/me — requires session cookie or Authorization: Bearer <token>
4. Logout
Session: POST /auth/session/logout
Token: POST /auth/logout
5. Forgot Password
POST /auth/password/forgot
{ "email": "admin@example.com" }
6. Reset Password
POST /auth/password/reset
{
"email": "admin@example.com",
"token": "reset-token",
"password": "newpassword",
"password_confirmation": "newpassword"
}
Passwords are capped at 128 characters to prevent bcrypt/argon2 resource exhaustion.
Two-Factor Authentication
Two-Factor Authentication is:
- Evaluated during login (backend only)
- Enforced before session/token is issued
- Protected against replay attacks (TOTP counter tracking)
Email OTP endpoints
POST /auth/session/2fa/email
POST /auth/session/2fa/verify-otp
POST /auth/session/2fa/disable
POST /auth/2fa/email
POST /auth/2fa/verify-otp
POST /auth/2fa/disable
Authenticator App (TOTP)
POST /auth/session/2fa/totp/enable
POST /auth/session/2fa/totp/verify
POST /auth/2fa/totp/enable
POST /auth/2fa/totp/verify
Recovery Codes
Used when a TOTP user has lost access to their authenticator app:
POST /auth/session/2fa/recovery
POST /auth/2fa/recovery
SMS-based OTP is intentionally excluded due to SIM-swap risk. 2FA management endpoints require the current password by default.
Configuration (config/authx.php)
use Rainwaves\LaraAuthSuite\Support\Enums\AuthMode; use Rainwaves\LaraAuthSuite\Support\Enums\AuthFeature; use Rainwaves\LaraAuthSuite\Support\Enums\TwoFactorChannel; return [ 'route_prefix' => 'auth', 'mode' => AuthMode::Session->value, // session | token | both 'user_model' => null, 'user_resource' => null, 'debug_ping' => false, // enable GET /auth/ping (disable in production) 'frontend' => [ 'password_reset_url' => env('AUTHX_FRONTEND_RESET_URL', '/auth/reset-password'), ], 'password_reset' => [ 'revoke_tokens' => true, // revoke all Sanctum PATs on reset 'invalidate_sessions' => true, // remove other database-backed sessions on reset 'notify_user' => true, // best-effort "your password was changed" notification ], // Devices is intentionally absent — trusted-device management is v2.2 // scope and isn't implemented yet. Add AuthFeature::Devices->value here // only once that ships. 'features' => [ AuthFeature::PasswordReset->value, AuthFeature::TwoFactor->value, AuthFeature::Tokens->value, ], '2fa' => [ 'channels' => [TwoFactorChannel::Email->value, TwoFactorChannel::Totp->value], 'enforcement' => 'optional', // off | optional | required 'remember_device_days' => 30, 'otp' => [ 'length' => 6, 'expiry_seconds' => 180, 'throttle_per_minute' => 5, ], 'totp_digits' => 6, 'totp_period' => 30, // RFC 6238 default (seconds) 'totp_window' => 1, // steps either side accepted (covers ±30s clock drift) 'verification_ttl_seconds' => 600, // how long verified 2FA state is trusted for token users 'recovery_codes_count' => 8, 'require_password_on_manage' => true, // gates disable + regenerate recovery codes 'require_password_on_enable' => true, // gates enrolling a new factor 'recent_password_ttl_seconds' => 300, // how long POST /auth/password/confirm stays valid ], 'tokens' => [ 'default_abilities' => ['auth:read'], // set ['*'] to restore the old unlimited default 'expiry_minutes' => 10080, // 7 days; set null to restore the old never-expires default 'default_name' => 'authx-client', 'ability_resolver' => null, // optional callable(Authenticatable $user): array<string> ], // Independent per-account and per-IP buckets — rotating one dimension // no longer bypasses the other's protection. Set either half to 0 to // disable just that dimension. 'throttle' => [ 'login_per_account' => 5, 'login_per_ip' => 20, 'password_reset_per_account' => 3, 'password_reset_per_ip' => 10, 'otp_send_per_account' => 3, 'otp_send_per_ip' => 10, 'two_factor_per_account' => 5, 'two_factor_per_ip' => 20, 'decay_seconds' => 60, ], 'registration' => [ 'enabled' => false, // opt-in — disabled by default 'issue_token_on_register' => false, // opt-in 'default_roles' => [], 'default_permissions' => [], 'allow_self_assign_roles' => false, 'allow_self_assign_permissions' => false, 'rules' => [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], 'password' => ['required', 'string', 'min:8', 'max:128', 'confirmed'], 'roles' => ['sometimes', 'array'], 'roles.*' => ['string'], 'permissions' => ['sometimes', 'array'], 'permissions.*' => ['string'], ], ], 'permissions' => [ 'enabled' => true, 'fail_open_when_tables_missing' => false, ], ];
Frontend Integration (SPA)
Frontend clients consume backend decisions. They do not determine authentication state.
The backend returns:
- Whether 2FA is required
- Which channel must be used
- A
pending_auth_id(token mode) to complete the flow - Whether the session/token is fully verified
Reference implementation: https://github.com/Magnificent-Big-J/lara-auth-suite-nuxt-demo
Roadmap
| Version | Feature |
|---|---|
| v1.0 | Token + session auth, password reset |
| v1.1 | Email OTP, TOTP, 2FA enforcement |
| v2.0 | Security hardening, TOTP replay protection, recovery codes, Laravel 13 |
| v2.x | Trusted devices, session/token audit |
Security
Report security issues privately to:
Do not open a public GitHub issue for security vulnerabilities.
License
MIT © Rainwaves
Credits
Built by Rainwaves Security-first authentication for serious Laravel applications.