pictastudio / auth
Opinionated API authentication and authorization package for Laravel Sanctum with Spatie roles and permissions.
Fund package maintenance!
Requires
- php: ^8.4
- illuminate/auth: ^12.0
- illuminate/console: ^12.0
- illuminate/contracts: ^12.0
- illuminate/http: ^12.0
- illuminate/routing: ^12.0
- illuminate/support: ^12.0
- laravel/sanctum: ^4.0
- spatie/laravel-permission: ^7.2
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^10.4
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
README
Opinionated API authentication and authorization for Laravel using Sanctum and Spatie roles/permissions.
Features
- Common API auth routes: login, logout, current user, forgot/reset password, email verification.
- Dual-mode Sanctum authentication:
- Stateful cookie/session auth for first-party frontend requests.
- Bearer token auth for third-party API consumers.
- Config-driven permission generation with pattern
{model}:{action}. - Config-driven role bootstrap with defaults:
root(all permissions)admin(all permissions)user(no permissions assigned by default)
- Global helper
auth_authorize(...)to run authorization checks from anywhere. - User model trait to bootstrap Sanctum + Spatie integration quickly.
Installation
composer require pictastudio/auth
Install Auth
php artisan auth:install
Configuration
Permissions are generated from config/picta-auth.php under picta-auth.permissions.
return [ 'permissions' => [ 'models' => [ 'post' => \App\Models\Post::class, \App\Models\Comment::class, ], 'actions' => [ 'view-any', 'view', 'create', 'show', 'update', 'delete', 'force-delete', 'restore', ], ], ];
For API-only projects, you can also point notification links to frontend routes:
return [ 'frontend_urls' => [ 'reset_password' => env('AUTH_LIBRARY_FRONTEND_RESET_PASSWORD_URL'), 'email_verification' => env('AUTH_LIBRARY_FRONTEND_EMAIL_VERIFICATION_URL'), ], ];
AUTH_LIBRARY_FRONTEND_RESET_PASSWORD_URL: frontend page that receivestokenandemailquery params.AUTH_LIBRARY_FRONTEND_EMAIL_VERIFICATION_URL: frontend page that receives signed verification query params (id,hash,expires,signature).- If
AUTH_LIBRARY_FRONTEND_RESET_PASSWORD_URLis not set and nopassword.resetroute exists, the package falls back toAPP_URL+picta-auth.routes.default_reset_password_path(default:/reset-password).
Password reset validation rules are configurable via picta-auth.password_rules:
return [ 'password_rules' => ['required', 'string', 'confirmed', 'min:12'], ];
AUTH_LIBRARY_ISSUE_TOKEN_BY_DEFAULT(optional): force login responses to always issue (true) or not issue (false) bearer tokens. By default, the package auto-detects: stateful frontend requests get cookie auth, non-stateful requests get bearer tokens.
If you publish the Bruno collection, create your local env file from the template:
cp bruno/auth/environments/Local.example.bru bruno/auth/environments/Local.bru
bruno/auth/environments/Local.bru is gitignored so personal values are not tracked.
Generated permission names follow:
{model}:{action}
Generate Permissions and Roles
php artisan auth:permissions:generate
This command keeps existing records and only creates missing permissions/roles.
User Model Trait
Use the package trait on your User model to get:
- Sanctum API tokens
- Spatie roles/permissions support
- Default guard resolution from
picta-auth.guard - Convenience method:
$user->canAuthorize($model, $action)
use Illuminate\Foundation\Auth\User as Authenticatable; use PictaStudio\Auth\Concerns\HasAuthFeatures; class User extends Authenticatable { use HasAuthFeatures; }
Global Helper
auth_authorize(\App\Models\Post::class, 'view', $user); auth_authorize(\App\Models\Post::class, 'update'); // defaults to auth()->guard()->user()
API Routes
Mounted under /api/auth by default:
POST /api/auth/loginGET /api/auth/mePOST /api/auth/logoutPOST /api/auth/forgot-passwordPOST /api/auth/reset-passwordPOST /api/auth/email/verification-notificationGET /api/auth/verify-email/{id}/{hash}
Login Modes
POST /api/auth/login supports both Sanctum modes:
- First-party frontend (stateful request,
Origin/Refererinsanctum.stateful): authenticates with cookies/session by default. - Third-party clients (non-stateful request): returns a bearer token by default.
Optional payload fields:
issue_token(boolean): force token issuance on/off.token_name(string): token name when issuing bearer tokens.
To use cookie-based SPA auth, make sure your frontend domain is in config/sanctum.php (sanctum.stateful) and keep picta-auth.routes.stateful_middleware enabled.
Frontend Integration (Cookie Auth)
Use this flow when your first-party frontend authenticates with cookies (Sanctum stateful mode) instead of bearer tokens.
1. Backend setup
Set your frontend as a stateful domain and allow cross-site credentials.
Example .env:
APP_URL=http://api.test SESSION_DRIVER=cookie SESSION_DOMAIN=.test SANCTUM_STATEFUL_DOMAINS=app.test,localhost:3000,127.0.0.1:3000
Example config/cors.php:
return [ 'paths' => ['api/*', 'api/auth/csrf-cookie'], 'allowed_methods' => ['*'], 'allowed_origins' => ['http://app.test', 'http://localhost:3000'], 'allowed_headers' => ['*'], 'supports_credentials' => true, ];
Notes:
- Keep
picta-auth.routes.stateful_middlewareenabled (default). - The Sanctum CSRF route is exposed under the same prefix as your auth routes (
GET /api/auth/csrf-cookieby default). - The package will default
POST /api/auth/loginto cookie auth for these stateful frontend requests. - For localhost-only development, you can leave
SESSION_DOMAINunset. - For production subdomains, use a shared session domain (for example
.example.com).
2. Frontend request flow
- Get CSRF cookie:
GET /api/auth/csrf-cookiewith credentials. - Login:
POST /api/auth/loginwith email/password and credentials. - Read current user:
GET /api/auth/mewith credentials. - Logout:
POST /api/auth/logoutwith credentials.
3. Frontend example (Axios)
import axios from 'axios'; const api = axios.create({ baseURL: 'http://api.test', withCredentials: true, withXSRFToken: true, }); export async function login(email: string, password: string) { await api.get('/api/auth/csrf-cookie'); await api.post('/api/auth/login', { email, password }); const { data } = await api.get('/api/auth/me'); return data.user; } export async function logout() { await api.post('/api/auth/logout'); }
4. Frontend example (Fetch)
const API_BASE = 'http://api.test'; export async function login(email: string, password: string) { await fetch(`${API_BASE}/api/auth/csrf-cookie`, { method: 'GET', credentials: 'include', }); await fetch(`${API_BASE}/api/auth/login`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); const meResponse = await fetch(`${API_BASE}/api/auth/me`, { method: 'GET', credentials: 'include', }); const me = await meResponse.json(); return me.user; }
5. If login still returns tokens
POST /api/auth/login can still issue bearer tokens when:
- The request is not detected as stateful (missing/mismatched
OriginorReferer). - You explicitly pass
issue_token: true. - You force token mode with
AUTH_LIBRARY_ISSUE_TOKEN_BY_DEFAULT=true.
Morph Map
Inside your AppServiceProvider add this to ensure the relation morph map is registered:
use Illuminate\Support\ServiceProvider; use PictaStudio\Auth\AuthServiceProvider; use Illuminate\Database\Eloquent\Relations\Relation; use App\Models\User; class AppServiceProvider extends ServiceProvider { public function boot() { Relation::morphMap([ 'user' => User::class, ]); } }
Testing
composer test