Authentication
The three credential systems — web sessions, REST API keys and mobile device tokens — plus OTP flows and where each is enforced.
Authentication
The platform has exactly three authentication mechanisms, each for a different caller. They are deliberately independent: API keys cannot log into panels, device tokens cannot call the REST API, and sessions are never used for machine callers.
Who this is for
Developers integrating with the API, debugging login failures, or extending auth.
Difficulty: Advanced · Time: about 12 minutes.
The three mechanisms
| Mechanism | Credential | Who uses it | Middleware | Where |
|---|---|---|---|---|
| Web session | Email/phone + password via Filament login form | Humans in /admin, /super-admin, /agency |
Filament auth + panel access checks | Panel routes |
| API key | Authorization: Bearer <key> |
Server-to-server integrations | api.key:<scope> → App\Http\Middleware\ApiKeyAuthentication |
/api/v1/* |
| Device token | Authorization: Bearer <token> |
The Android app | App\Http\Middleware\DeviceAuthenticate |
/api/v1/mobile/* (except login/OTP/config) |
Web sessions (Filament panels)
- Login lives on the panel's own login page; credentials are submitted via Livewire.
- Login lockout: 5 failed attempts → 15 minutes. Failures are recorded in
login_attempts(LoginAttempt::recordFailureAndMaybeLock). Because Filament 4 submits through/livewire/update(not the panel login URL), panels enforce the lockout insideApp\Filament\Auth\Pages\Login, which hooks Livewire'sauthenticate(). Thelogin.lockoutalias (EnforceLoginLockout) exists for any custom non-Filament login route. - Super Admin access additionally has the optional IP allowlist —
sa.ip.allowlist→EnforceSuperAdminIpAllowlist. - Guests hitting an expired non-panel URL are redirected to the panel login (configured in
bootstrap/app.php).
OTP login for the mobile app (pre-device-auth endpoints)
| Endpoint | Purpose |
|---|---|
GET /api/v1/mobile/auth-config |
Which login methods this workspace supports (password/email OTP/phone OTP, Firebase options when configured) |
POST /api/v1/mobile/login |
Email or phone + password + device_uuid → device token |
POST /api/v1/mobile/otp/start |
Send a one-time code (email, workspace SMS, or Firebase) |
POST /api/v1/mobile/otp/verify |
Verify code or Firebase ID token → device token |
Details enforced in App\Http\Controllers\Api\Mobile\AuthController and its rate limiters:
- OTP codes are stored HMAC-SHA256-hashed with a random salt — the plaintext is never persisted.
- Verify attempts are capped (5 per code); issuance is limited per identifier (3 per 10 minutes) and per IP (10 per 10 minutes), on top of a coarse route throttle (
throttle:10,1). - Phone OTP via the workspace's Firebase project is verified server-side (Google Identity Toolkit
getAccountInfo) — no Admin SDK orgoogle-services.jsonis required. - If Firebase is not configured, phone OTP falls back to the workspace's SMS provider.
API keys (REST /api/v1)
- Keys are per-tenant credentials, managed in the CRM; the model is
App\Models\ApiKey(ApiKey::findByKey($plainToken)). - The guard is
ApiKeyAuthentication, invoked via the scope-taking alias, e.g.:
Scopes followRoute::get('leads', [LeadController::class, 'index']) ->middleware('api.key:read:leads');<action>:<resource>(read:leads,write:leads,delete:leads,read:forms, …). A missing scope → 403Insufficient scope. Required: …. - The key must be active and its workspace subscription must be usable:
TenantSubscriptionState::of($tenant)->isActive()(on-trial or active-paid). Otherwise the API returns 403 even with a valid key. - On success the middleware binds
app('current_tenant')andtenant_idso the tenant scope works for the whole request. - Rate limits are enforced in two tiers with response headers (
X-RateLimit-Limit,-Remaining,-Reset):- per key:
api_keys.rate_limit_per_hour(fixed hourly window); - per tenant:
tenants.settings.api.rate_limit_per_houroverride, elseconfig('leadhub.api.tenant_rate_limit_multiplier')× per-key (default 5×). Exceeding either returns 429 withRetry-After.
- per key:
- Public health check:
GET /api/v1/health. The interactive spec is at/api/docs.
Device tokens (mobile /api/v1/mobile)
Per-user, per-device credentials minted at login:
- The app posts
POST /api/v1/mobile/login(orotp/verify) withemail|phone, password/code anddevice_uuid. - The server registers/updates the row in
devicesand mints a token; onlydevice_tokens.token_hashis stored. Tokens live up to 90 days (TOKEN_TTL_DAYS = 90). - Every subsequent request carries
Authorization: Bearer <token>throughDeviceAuthenticate, which:- looks the token up without the tenant scope (the lookup must span tenants), matching
DeviceToken::hashToken($plain); - rejects unusable/expired/revoked tokens and suspended users with the mobile error envelope
{"error":{"message","code":"UNAUTHORIZED"}}; - resolves the device's user and tenant, then binds
auth()->user(), the user resolver, andapp('current_tenant')so tenant scoping and per-user lead restriction behave exactly like the panel; - debounces
last_used_at/last_seen_atwrites to at most once per minute per device.
- looks the token up without the tenant scope (the lookup must span tenants), matching
POST /api/v1/mobile/logoutrevokes the token for that device.device/heartbeatanddevice/registerkeep device metadata fresh (app version, push token).
Route throttles: login, otp/start, otp/verify each have throttle:10,1 at the route level; in-controller limiters add the per-identifier/per-IP backstops above.
Where each is enforced (quick reference)
| Control | Class |
|---|---|
| REST key guard | App\Http\Middleware\ApiKeyAuthentication (alias api.key) |
| Mobile device guard | App\Http\Middleware\DeviceAuthenticate |
| Panel lockout | App\Filament\Auth\Pages\Login + App\Models\LoginAttempt |
| Custom-route lockout | App\Http\Middleware\EnforceLoginLockout (alias login.lockout) |
| SA IP allowlist | App\Http\Middleware\EnforceSuperAdminIpAllowlist (alias sa.ip.allowlist) |
| Tenant context for web | App\Http\Middleware\ResolveTenant (+ EnforceTenantScope via tenant.scope) |
| Subscription gate | App\Services\TenantSubscriptionState / EnforceSubscription |
Related guides
Was this page helpful?
Thanks — noted. ✦