joe-404/laravel-auth
最新稳定版本:v2.0.0
Composer 安装命令:
composer require joe-404/laravel-auth
包简介
Drop-in, config-driven authentication library for Laravel 13. Registration, OTP + magic-link verification, login, password reset, sessions, API tokens, Google OAuth, and real-time Reverb verification — all through a single JSON API.
关键字:
README 文档
README
A drop-in, config-driven authentication library for Laravel 13. Register, verify, log in, reset passwords, manage sessions, issue API tokens, and sign in with Google — all through a single JSON API with zero frontend coupling.
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Authentication Modes
- API Reference
- Configuration
- Customization
- Events
- Security Design
- Testing
- License
Features
| Feature | Details |
|---|---|
| Registration | Email-only initiation → OTP or magic-link verification → password set after email proof |
| Email verification | OTP code, magic link, or both in a single email |
| Login | Password-based; session cookie or Bearer token depending on auth mode |
| Token refresh | Dedicated refresh tokens (separate table, one-time-use, atomic rotation) |
| Password reset | OTP or magic link; independently configurable from registration |
| Password change | Authenticated; optionally revokes all other sessions |
| Session management | Track device, browser, OS, IP, city, country; revoke individual or all sessions |
| Google OAuth | Sign-in with Google; safe account-linking with inbox-confirmation email |
| API tokens | Long-lived, scoped tokens for third-party integrations (opt-in feature) |
| Rate limiting | Per-IP + per-email; independently configurable per endpoint |
| Account lockout | Temporary lockout after repeated failed login attempts |
| New device alerts | Email notification when a user logs in from an unrecognised browser/OS |
| Reverb WebSocket | Optional real-time verification status push |
| Response envelope | Uniform { success, message, data } / { success, message, errors } on every response |
Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.2 |
| Laravel | ^12.0 or ^13.0 |
| Laravel Sanctum | ^4.0 |
| Laravel Socialite | ^5.0 |
| Spatie Permission | ^6.0 |
| Redis (recommended) | phpredis or predis |
Installation
1. Install via Composer
composer require joe-404/laravel-auth
2. Run the installer
php artisan auth:install
This command publishes the config file, migrations, and email views, and walks you through the minimal .env setup.
3. Run migrations
php artisan migrate
Six tables are created:
| Table | Purpose |
|---|---|
auth_otp_codes |
OTP codes and magic-link tokens (stored as SHA-256 hashes) |
auth_sessions_extended |
Device and session tracking per user |
auth_refresh_tokens |
Refresh tokens (hashed, separate from Sanctum access tokens) |
auth_social_accounts |
Linked OAuth provider accounts |
auth_api_tokens |
Long-lived API tokens (when feature is enabled) |
users (altered) |
Adds last_login_at and is_active columns |
4. Seed roles
php artisan db:seed --class=AuthRolesSeeder
Creates super-admin, admin, and user roles via Spatie Permission.
5. Configure Sanctum
In config/sanctum.php, make sure your frontend domain is in the stateful list:
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),
Quick Start
Minimal .env
AUTH_MODE=both AUTH_VERIFICATION_METHOD=both MAIL_MAILER=smtp MAIL_FROM_ADDRESS=hello@yourapp.com MAIL_FROM_NAME="${APP_NAME}"
End-to-end example with curl
# 1. Initiate registration (email only — no password yet) curl -sX POST http://localhost/auth/register \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"email":"user@example.com"}' # → { "data": { "temp_token": "uuid", "method": "both", "expires_in": 10 } } # 2. Verify with the OTP code sent to the email curl -sX POST http://localhost/auth/register/verify-otp \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com","otp":"482910"}' # → { "data": { "completion_token": "uuid" } } # 3. Set password and complete registration curl -sX POST http://localhost/auth/register/complete \ -H "Content-Type: application/json" \ -d '{"completion_token":"uuid","password":"Secret123!","password_confirmation":"Secret123!"}' # → { "data": { "user": {...}, "token": "1|abc...", "refresh_token": "xyz..." } } # 4. Login curl -sX POST http://localhost/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com","password":"Secret123!"}' # → { "data": { "user": {...}, "token": "...", "refresh_token": "..." } }
Authentication Modes
Set AUTH_MODE to control how credentials are issued after login:
| Mode | Behaviour | Best for |
|---|---|---|
web |
Session cookie only, no tokens | Traditional web apps, cookie-based SPAs |
api |
Bearer token always | Pure API backends, mobile apps |
both |
Auto-detect per request (default) | One backend serving both mobile and browser |
How both mode detects the client:
- Request has
X-Client-Type: mobileheader → issues Bearer token (mobile TTL) AUTH_SPA_TOKEN=trueand noX-Client-Type→ issues Bearer token (SPA TTL)- Otherwise → sets a session cookie (no token)
In web and both (session) modes, SPA clients should:
- Call
GET /sanctum/csrf-cookiefirst - Send the CSRF cookie (
X-XSRF-TOKENheader) with all state-mutating requests
API Reference
All routes are prefixed with /auth. All responses use the envelope:
// Success { "success": true, "message": "...", "data": { ... } } // Error { "success": false, "message": "...", "errors": { "field": ["..."] } }
Registration
Registration is a three-step flow that proves email ownership before accepting a password, preventing pre-account takeover attacks.
Step 1 — Initiate
POST /auth/register
Accepts email plus any extra fields defined in auth_system.registration.extra_fields_rules.
Request
{
"email": "user@example.com"
}
Response 201
{
"success": true,
"message": "Verification sent. Please check your email.",
"data": {
"temp_token": "550e8400-e29b-41d4-a716-446655440000",
"method": "both",
"expires_in": 10
}
}
temp_token— subscribe toEcho.private("auth.verification.{temp_token}")for real-time status (requires Reverb)method—"otp","magic_link", or"both"expires_in— minutes until the OTP expires
Step 2a — Verify with OTP
POST /auth/register/verify-otp
Request
{
"email": "user@example.com",
"otp": "482910"
}
Response 200
{
"success": true,
"message": "Email verified. Please set your password.",
"data": {
"completion_token": "a7f3d9c2-1234-5678-abcd-ef0123456789"
}
}
Step 2b — Verify with magic link
The user clicks the link in their email. The library handles this route automatically.
GET /auth/register/verify-magic/{token}
Returns the same { "completion_token": "..." } payload as OTP verification.
Frontend target mode: when
AUTH_MAGIC_LINK_TARGET=frontend, the email link points toAUTH_FRONTEND_VERIFY_URL?token=xxx. Your frontend extracts the token and callsGET /auth/register/verify-magic/{token}itself.
Step 3 — Complete registration
POST /auth/register/complete
Request
{
"completion_token": "a7f3d9c2-1234-5678-abcd-ef0123456789",
"password": "Secret123!",
"password_confirmation": "Secret123!"
}
Response 201
{
"success": true,
"message": "Registration complete.",
"data": {
"user": { "id": 1, "name": "user", "email": "user@example.com" },
"token": "1|abc123...",
"refresh_token": "def456..."
}
}
token and refresh_token are null in web/session mode; the session cookie is set automatically.
Resend verification
POST /auth/email/resend-verification
Request
{ "email": "user@example.com" }
Always returns success to prevent email enumeration.
Login & Logout
Login
POST /auth/login
Request
{
"email": "user@example.com",
"password": "Secret123!"
}
Add X-Client-Type: mobile to receive a Bearer token in both mode.
Response 200
{
"success": true,
"message": "Login successful.",
"data": {
"user": { "id": 1, "email": "user@example.com" },
"token": "1|abc123...",
"refresh_token": "def456..."
}
}
Error codes
| HTTP | Reason |
|---|---|
401 |
Invalid credentials |
403 |
Account inactive or email not verified |
423 |
Account locked out |
429 |
Rate limit exceeded |
Logout
POST /auth/logout
Revokes the current token (and its paired refresh token) or invalidates the session. Requires authentication.
Logout all devices
POST /auth/logout/all
Revokes all tokens and sessions for the authenticated user. The current session is preserved.
Get current user
GET /auth/me
Response 200
{
"success": true,
"data": {
"user": { "id": 1, "email": "user@example.com" },
"roles": ["user"],
"permissions": ["read:posts"],
"active_sessions": 3
}
}
Token Refresh
Exchange an expired access token for a new pair. The old refresh token is consumed atomically (one-time use). Concurrent refresh requests are safe.
POST /auth/token/refresh
Request
{ "refresh_token": "def456..." }
Response 200
{
"success": true,
"data": {
"user": { "..." },
"token": "2|xyz789...",
"refresh_token": "ghi012..."
}
}
Error codes
| HTTP | Reason |
|---|---|
401 |
Token invalid or already consumed |
401 |
Token expired — user must log in again |
Password Reset
Step 1 — Request reset
POST /auth/password/forgot
Request
{ "email": "user@example.com" }
Always returns success (prevents enumeration). Sends OTP or magic link per AUTH_PASSWORD_RESET_METHOD.
Step 2a — Reset with OTP
POST /auth/password/reset/otp
Request
{
"email": "user@example.com",
"otp": "719283",
"password": "NewSecret123!",
"password_confirmation": "NewSecret123!"
}
Step 2b — Reset with magic link
User clicks the link → library validates it → issues a short-lived reset_token (15 min).
GET /auth/password/reset/magic/{token}
Response 200
{
"success": true,
"data": { "reset_token": "uuid" }
}
Then submit the new password:
POST /auth/password/reset/confirm
Request
{
"reset_token": "uuid",
"password": "NewSecret123!",
"password_confirmation": "NewSecret123!"
}
All sessions and tokens are revoked on successful reset.
Password Change
Requires authentication.
POST /auth/password/change
Request
{
"current_password": "Secret123!",
"new_password": "NewSecret456!",
"new_password_confirmation": "NewSecret456!",
"logout_all": true
}
logout_all— iftrue, all other sessions and tokens are revoked; the current session is preserved
Sessions
Requires authentication.
List sessions
GET /auth/sessions
Response 200
{
"success": true,
"data": {
"sessions": [
{
"id": 1,
"platform": "web",
"browser": "Chrome",
"os": "Windows",
"ip_address": "203.0.113.10",
"city": "Beirut",
"country": "LB",
"last_active_at": "2026-05-09T14:23:00Z",
"is_current": true
}
]
}
}
Revoke a session
DELETE /auth/sessions/{id}
Returns 403 if the session does not belong to the authenticated user.
Google OAuth
Step 1 — Get redirect URL
GET /auth/social/google/redirect
- Browser clients get a
302redirect to Google - JSON/XHR clients get
{ "redirect_url": "https://accounts.google.com/..." }
Step 2 — Handle callback
Google redirects back to:
GET /auth/social/google/callback
Happy path (existing or new account):
{
"success": true,
"message": "Logged in with Google successfully.",
"data": {
"user": { "..." },
"token": "1|abc123...",
"refresh_token": "def456..."
}
}
Account-linking required (Google email matches an existing local account but no social link exists):
{
"success": true,
"message": "An email was sent to confirm linking your account.",
"data": { "email": "user@example.com" }
}
A signed confirmation email is sent. The user clicks it to approve the link — the library never auto-links based on email match alone.
Step 3 — Confirm account link (email click)
GET /auth/social/{provider}/link/confirm/{token}
After confirming, the user is logged in and redirected to AUTH_SOCIAL_FRONTEND_URL (or a JSON response if not set).
Required .env
AUTH_GOOGLE_ENABLED=true AUTH_GOOGLE_CLIENT_ID=123456789.apps.googleusercontent.com AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-xxxx AUTH_GOOGLE_REDIRECT=https://yourapp.com/auth/social/google/callback AUTH_SOCIAL_FRONTEND_URL=https://yourapp.com/auth/callback # optional
API Tokens
Long-lived, scoped tokens for third-party integrations (CI/CD pipelines, scripts, external services). Disabled by default — enable with AUTH_API_TOKENS_ENABLED=true.
These tokens use the format auth_at_{base64}, are stored in auth_api_tokens, and are completely separate from Sanctum session tokens.
User endpoints (requires auth)
GET /auth/api-tokens # List your tokens
POST /auth/api-tokens # Create a token
DELETE /auth/api-tokens/{id} # Revoke a token
Create a token — Request
{
"name": "My CI Pipeline",
"abilities": ["read", "deploy"],
"expires_in_days": 90
}
Response 201
{
"success": true,
"data": {
"raw_token": "auth_at_...",
"token": { "id": 1, "name": "My CI Pipeline", "abilities": ["read","deploy"], "expires_at": "2026-08-07T00:00:00Z" }
}
}
Store
raw_tokensecurely — it is shown only once.
Admin endpoints (requires super-admin or admin role)
GET /auth/admin/api-tokens # List all tokens
POST /auth/admin/api-tokens # Create a system-level token
PATCH /auth/admin/api-tokens/{id} # Update abilities / expiry
DELETE /auth/admin/api-tokens/{id} # Revoke any token
Configuration
After running php artisan auth:install, edit config/auth_system.php. Every option has a corresponding .env variable.
Complete .env reference
# ── Core ───────────────────────────────────────────────────────────────────── AUTH_MODE=both # api | web | both (default: both) AUTH_SPA_TOKEN=false # true = SPA clients get Bearer token in 'both' mode AUTH_REQUIRE_VERIFICATION=true # false = allow login without email verification # ── Verification ───────────────────────────────────────────────────────────── AUTH_VERIFICATION_METHOD=both # otp | magic_link | both AUTH_OTP_LENGTH=6 # digits in the OTP code (4–8) AUTH_OTP_EXPIRY=10 # minutes until OTP expires AUTH_OTP_MAX_ATTEMPTS=5 # wrong guesses before OTP is invalidated AUTH_MAGIC_EXPIRY=30 # minutes until magic link expires AUTH_MAGIC_LINK_TARGET=backend # backend | frontend AUTH_FRONTEND_VERIFY_URL= # e.g. https://yourapp.com/verify-email AUTH_FRONTEND_RESET_URL= # e.g. https://yourapp.com/reset-password AUTH_PENDING_TTL=60 # minutes to keep pending registration in cache # ── Password Reset ──────────────────────────────────────────────────────────── AUTH_PASSWORD_RESET_METHOD= # null = inherit AUTH_VERIFICATION_METHOD # or: otp | magic_link | both # ── Password Policy ─────────────────────────────────────────────────────────── AUTH_PASSWORD_MIN=8 # minimum password length AUTH_PASSWORD_UPPERCASE=false # require at least one uppercase letter AUTH_PASSWORD_NUMBER=false # require at least one digit AUTH_PASSWORD_SPECIAL=false # require at least one symbol # ── Token TTL (in minutes) ──────────────────────────────────────────────────── AUTH_TOKEN_TTL_MOBILE=10080 # mobile access token — 7 days AUTH_REFRESH_TTL_MOBILE=43200 # mobile refresh token — 30 days AUTH_TOKEN_TTL_SPA=1440 # SPA access token — 24 hours AUTH_REFRESH_TTL_SPA=10080 # SPA refresh token — 7 days AUTH_TOKEN_TTL_API=525600 # API access token — 365 days AUTH_REFRESH_TTL_API=0 # API refresh token — 0 = never expires # ── Rate Limits (format: "max_attempts:decay_minutes") ─────────────────────── AUTH_RATE_REGISTER=5:1 # 5 registrations per minute AUTH_RATE_LOGIN=5:1 # 5 login attempts per minute AUTH_RATE_OTP_VERIFY=10:5 # 10 OTP guesses per 5 minutes AUTH_RATE_OTP_SEND=3:1 # 3 OTP resend requests per minute AUTH_RATE_PASSWORD_RESET=3:1 # 3 reset requests per minute # ── Security ───────────────────────────────────────────────────────────────── AUTH_NOTIFY_NEW_DEVICE=true # email alert on new browser/OS login AUTH_LOCKOUT_ENABLED=true # lock account after repeated failures AUTH_LOCKOUT_MAX=10 # failed attempts before lockout AUTH_LOCKOUT_DECAY=15 # minutes the lockout lasts # ── Roles ──────────────────────────────────────────────────────────────────── AUTH_DEFAULT_ROLE=user # role auto-assigned on registration # ── OTP delivery channel ────────────────────────────────────────────────────── AUTH_OTP_CHANNEL=email # email (default) | App\Channels\SmsOtpChannel # ── Google OAuth ───────────────────────────────────────────────────────────── AUTH_GOOGLE_ENABLED=false AUTH_GOOGLE_CLIENT_ID=123456789.apps.googleusercontent.com AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-xxxx AUTH_GOOGLE_REDIRECT=https://yourapp.com/auth/social/google/callback AUTH_SOCIAL_FRONTEND_URL= # redirect target after social link confirmation # ── API Tokens ──────────────────────────────────────────────────────────────── AUTH_API_TOKENS_ENABLED=false # enable the long-lived API token system # ── Queue ──────────────────────────────────────────────────────────────────── AUTH_QUEUE_CONNECTION= # null = app default queue AUTH_QUEUE_NAME=auth-maintenance # queue name for maintenance jobs # ── Reverb (real-time WebSocket) ────────────────────────────────────────────── AUTH_REVERB_ENABLED=false # ── Response formatter ──────────────────────────────────────────────────────── AUTH_RESPONSE_FORMATTER= # null = default { success, message, data }
Customization
Extra Registration Fields
Add custom fields to registration without touching library code.
Option A — simple rule strings (in config/auth_system.php):
'registration' => [ 'extra_fields_rules' => [ 'phone' => 'required|string|max:20', 'country' => 'required|string|size:2', ], ],
Ensure the field names are in your User model's $fillable array. Their validated values are passed directly to User::create().
Option B — custom FormRequest (for complex rules, custom messages):
// app/Http/Requests/MyRegisterRequest.php use Joe404\LaravelAuth\Http\Requests\RegisterRequest; use Illuminate\Validation\Rule; class MyRegisterRequest extends RegisterRequest { public function rules(): array { return array_merge(parent::rules(), [ 'phone' => ['required', 'string', Rule::unique('users')], ]); } public function messages(): array { return [ 'phone.unique' => 'That phone number is already registered.', ]; } }
// config/auth_system.php 'registration' => [ 'request_class' => \App\Http\Requests\MyRegisterRequest::class, ],
Custom OTP Channel (SMS, WhatsApp…)
Replace the built-in email delivery with any channel by implementing OtpChannelContract:
use Joe404\LaravelAuth\Contracts\OtpChannelContract; class SmsOtpChannel implements OtpChannelContract { public function send(string $recipient, string $code, string $type, array $context = []): void { // $code — plain OTP digits (e.g. "482910") or the full magic link URL // $type — 'email_verify' | 'magic_link_verify' | 'password_reset' | 'magic_link_reset' TwilioClient::messages->create($recipient, [ 'from' => config('services.twilio.from'), 'body' => "Your code: {$code}", ]); } }
For channels that can combine OTP + magic link into a single message, implement CombinedOtpChannelContract:
use Joe404\LaravelAuth\Contracts\CombinedOtpChannelContract; class WhatsAppChannel implements CombinedOtpChannelContract { public function send(string $recipient, string $code, string $type, array $context = []): void { // Fallback: code-only delivery } public function sendCombined(string $recipient, string $code, string $url, string $type, array $context = []): void { // Single message with both the OTP code and the clickable magic link } }
Register in config:
// config/auth_system.php 'otp_channel' => [ 'driver' => \App\Channels\SmsOtpChannel::class, ],
Custom Response Format
Every response passes through a formatter. Swap the envelope to match your API conventions:
use Joe404\LaravelAuth\Contracts\ResponseFormatterContract; class MyFormatter implements ResponseFormatterContract { public function format(bool $success, string $message, array $data, array $errors): array { return [ 'ok' => $success, 'msg' => $message, 'payload' => $data ?: $errors, ]; } }
Register via config (recommended) or service container:
// Option 1 — config/auth_system.php 'response' => [ 'formatter' => \App\Auth\MyFormatter::class, ], // Option 2 — AppServiceProvider (config takes priority) use Joe404\LaravelAuth\Contracts\ResponseFormatterContract; $this->app->bind(ResponseFormatterContract::class, \App\Auth\MyFormatter::class);
Custom Email Templates
Option 1 — Blade views (recommended for styling):
php artisan vendor:publish --tag=auth-views
Edit files in resources/views/vendor/laravel-auth/emails/:
| File | |
|---|---|
otp-verify.blade.php |
OTP code — registration |
otp-reset.blade.php |
OTP code — password reset |
magic-link-verify.blade.php |
Magic link — registration |
magic-link-reset.blade.php |
Magic link — password reset |
otp-verify-combined.blade.php |
OTP + magic link in one email (method=both, registration) |
otp-reset-combined.blade.php |
OTP + magic link in one email (method=both, password reset) |
Option 2 — custom notification class (for full control over the mailable):
Your class receives ($code, $type, $context) in its constructor. For combined: ($code, $url, $type, $context).
// config/auth_system.php 'mail' => [ 'otp_reset_notification' => \App\Notifications\MyResetEmail::class, ],
You can mix — override only specific emails and let the library handle the rest.
Events
| Event | When fired | Payload |
|---|---|---|
UserRegistered |
Registration initiated | $user |
EmailVerified |
Email verified and account created | $user, $completionToken |
UserLoggedIn |
Successful login | $user, $request |
UserLoggedOut |
Any logout | — |
PasswordChanged |
Password reset or changed | $user |
SuspiciousLoginDetected |
Login from unrecognised device | $user, $ip, $browser, $os, $city, $country |
Example — send a welcome email after registration:
use Joe404\LaravelAuth\Events\EmailVerified; use Illuminate\Support\Facades\Event; use App\Mail\WelcomeMail; use Illuminate\Support\Facades\Mail; Event::listen(EmailVerified::class, function (EmailVerified $event): void { Mail::to($event->user)->send(new WelcomeMail($event->user)); });
Security Design
Registration — no pre-account takeover
The three-step flow ensures that the person who proves email ownership is the one who sets the password:
- Initiate — only the email is captured; nothing sensitive is cached
- Verify — proves inbox access; issues a
completion_token(15-min TTL) - Complete — whoever holds the
completion_tokensets the password
An attacker who registers with a victim's email receives a temp_token but never gets the completion_token — that is given to whoever completes the inbox challenge. The victim who clicks the verification link sets their own password. Neither party can impersonate the other.
Token storage
All tokens are stored as SHA-256 hashes — plain values are never written to the database:
| Token | Table | Type |
|---|---|---|
| Access token | personal_access_tokens |
Sanctum SHA-256 |
| Refresh token | auth_refresh_tokens |
SHA-256, one-time use |
| OTP code | auth_otp_codes |
SHA-256 |
| Magic link UUID | auth_otp_codes |
SHA-256 |
| API token | auth_api_tokens |
SHA-256 |
Refresh token rotation
Refresh tokens live in a dedicated auth_refresh_tokens table, completely separate from Sanctum's personal_access_tokens. Each token is:
- One-time use — consumed via
DB::transaction()+SELECT FOR UPDATEto prevent concurrent reuse - Paired — each refresh token points to exactly one access token; only that pair is revoked on rotation
OTP brute-force protection
Each wrong OTP guess increments a failed_attempts counter on the active OTP row (atomic INCREMENT to prevent races). When the counter reaches AUTH_OTP_MAX_ATTEMPTS (default 5), the OTP is invalidated and the user must request a new one.
Rate limiting
All auth endpoints are rate-limited per IP and per email address independently. Exceeding either limit returns HTTP 429.
Account lockout
Repeated failed login attempts trigger a time-limited lockout (AUTH_LOCKOUT_MAX failures → locked for AUTH_LOCKOUT_DECAY minutes). This is separate from rate limiting — it persists even across slow, distributed attempts.
OAuth account linking
When a Google account's email matches an existing local account, the library never auto-links based on email alone. A signed confirmation email is sent to the registered address; only after the legitimate inbox owner clicks the link is the social account linked, preventing account takeover via OAuth email spoofing.
Testing
composer test
The test suite uses Pest with RefreshDatabase, Mail::fake(), and Queue::fake(). No real emails or HTTP calls are made.
License
MIT. See LICENSE.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 11
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-05-08