rainwaves/lara-auth-suite

Laravel API auth suite: Sanctum tokens & session modes, password reset, and 2FA.

Maintainers

Package info

github.com/Magnificent-Big-J/lara-auth-suite

pkg:composer/rainwaves/lara-auth-suite

Transparency log

Statistics

Installs: 948

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 2

v2.1.4 2026-08-17 08:03 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:

security@rainwaves.dev

Do not open a public GitHub issue for security vulnerabilities.

License

MIT © Rainwaves

Credits

Built by Rainwaves Security-first authentication for serious Laravel applications.