Spa Auth Development
gdarko/laravel-vue-starter
Activate when working on SPA authentication flow, Sanctum cookie-based auth, Vue Router guards, auth store, login/register/password reset pages, or CORS/session configuration.
Laravel security best practices — authentication, authorization, Eloquent safety, CSRF, XSS prevention, API security, and secure deployment configurations.
$ npx skills add affaan-m/ECC --skill laravel-security -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install affaan-m/ECC laravel-security --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/affaan-m/ECC.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/laravel-security .claude/skills/laravel-security && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "laravel-security" agent skill from https://github.com/affaan-m/ECC/tree/main/skills/laravel-security into .claude/skills/laravel-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "laravel-security", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/affaan-m/ECC/tree/main/skills/laravel-securityType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add affaan-m/ECC --skill laravel-security -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install affaan-m/ECC laravel-security --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/affaan-m/ECC.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/laravel-security .agents/skills/laravel-security && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "laravel-security" agent skill from https://github.com/affaan-m/ECC/tree/main/skills/laravel-security into .agents/skills/laravel-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "laravel-security", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add affaan-m/ECC --skill laravel-security -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install affaan-m/ECC laravel-security --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/affaan-m/ECC.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/laravel-security .cursor/skills/laravel-security && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "laravel-security" agent skill from https://github.com/affaan-m/ECC/tree/main/skills/laravel-security into .cursor/skills/laravel-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "laravel-security", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/affaan-m/ECC.git --path skills/laravel-security--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add affaan-m/ECC --skill laravel-security -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install affaan-m/ECC laravel-security --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/affaan-m/ECC.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/laravel-security .gemini/skills/laravel-security && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "laravel-security" agent skill from https://github.com/affaan-m/ECC/tree/main/skills/laravel-security into .gemini/skills/laravel-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "laravel-security", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install affaan-m/ECC laravel-securityInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add affaan-m/ECC --skill laravel-security -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/affaan-m/ECC.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/laravel-security .github/skills/laravel-security && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "laravel-security" agent skill from https://github.com/affaan-m/ECC/tree/main/skills/laravel-security into .github/skills/laravel-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "laravel-security", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add affaan-m/ECC --skill laravel-security -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install affaan-m/ECC laravel-security --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/affaan-m/ECC.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/laravel-security .opencode/skills/laravel-security && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "laravel-security" agent skill from https://github.com/affaan-m/ECC/tree/main/skills/laravel-security into .opencode/skills/laravel-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "laravel-security", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
laravel-securityLaravel security best practices — authentication, authorization, Eloquent safety, CSRF, XSS prevention, API security, and secure deployment configurations.
Laravel Security is an agent skill from affaan-m/ECC. Laravel security best practices — authentication, authorization, Eloquent safety, CSRF, XSS prevention, API security, and secure deployment configurations. Use when reviewing Laravel auth, Eloquent safety, CSRF, XSS, API security, or deployment configuration.
Its SKILL.md is about 6.7k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Backend & APIs, covering Backend development, Web application vulnerabilities and Authentication. It works with Laravel. The repository describes itself as: The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond. The licence is MIT.
Read from SKILL.md and the folder at commit 2d515e4. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
composerphpFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
APP_KEYDB_PASSWORDAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYSTRIPE_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Laravel Security loads about 6.7k tokens when it runs. Until then it costs about 69 tokens; SKILL.md has 301 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
# NEVER commit .env to version control# .gitignore already includes .env by default# .env file (NEVER commit)# .gitignore includes .env by default| `.env` not committed | Verify `.gitignore` includes `.env` |Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from affaan-m/ECC at commit 2d515e4, republished under its MIT licence (© affaan-m). 301 words, ~6,706 tokens.
.claude/skills/laravel-security/SKILL.md (or your agent's skills folder).Comprehensive security guidelines for Laravel applications to protect against common vulnerabilities.
// config/app.php
'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false), // CRITICAL: Never true in production
'key' => env('APP_KEY'), // Must be set: php artisan key:generate
// config/session.php
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'lax',
// Verify APP_KEY is set at boot
// bootstrap/app.php or a service provider
if (empty(config('app.key'))) {
throw new RuntimeException('APP_KEY is not set. Run: php artisan key:generate');
}# NEVER commit .env to version control
# .gitignore already includes .env by default
# Use .env.example with placeholders instead
DB_PASSWORD=
APP_KEY=
SANCTUM_TOKEN_PREFIX=
# Validate required variables at boot
// In AppServiceProvider::boot()
$requiredKeys = ['app.key', 'database.connections.mysql.database', 'database.connections.mysql.username'];
foreach ($requiredKeys as $key) {
if (empty(config($key))) {
throw new RuntimeException("Missing required config key: {$key}");
}
}// AppServiceProvider::boot() or middleware
if (app()->environment('production')) {
URL::forceScheme('https');
request()->server->set('HTTPS', 'on');
}
// config/app.php for trusted proxies (load balancers)
// Use specific IP ranges — * trusts all, allowing X-Forwarded-* spoofing
// AWS: '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'
'trusted_proxies' => ['10.0.0.0/8', '172.16.0.0/12'],
// Force HTTPS in production via middleware
// app/Http/Middleware/ForceHttps.php
public function handle($request, Closure $next)
{
if (!$request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : ''
)));
'expiration' => 60 * 24, // Token expiration in minutes (null = never)
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
// Issuing tokens with abilities
$token = $user->createToken('api-token', ['read', 'write'])->plainTextToken;
// Validate abilities on routes
Route::middleware('auth:sanctum')->group(function () {
Route::get('/orders', function () {
// User must have 'read' ability
abort_unless(Auth::user()->tokenCan('read'), 403);
// ...
})->middleware('abilities:read');
Route::post('/orders', function () {
// User must have 'write' ability
abort_unless(Auth::user()->tokenCan('write'), 403);
// ...
})->middleware('abilities:write');
});// config/hashing.php
// Default is bcrypt. Argon2id is stronger.
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12), // Increase for stronger hashing
],
'argon' => [
'memory' => 65536,
'threads' => 4,
'time' => 4,
],
// Password validation in RegisterRequest
public function rules(): array
{
return [
'password' => [
'required',
'confirmed',
Password::min(12)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(), // Checks haveibeenpwned
],
];
}
// Rate limit login attempts
// App\Http\Controllers\Auth\AuthenticatedSessionController
protected function authenticated(Request $request, $user)
{
if ($user->wasRecentlyLockedOut()) {
// Notify user of suspicious login
$user->notify(new SuspiciousLoginNotification($request->ip()));
}
}// config/session.php
'driver' => env('SESSION_DRIVER', 'database'), // database/redis > file
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
'encrypt' => env('SESSION_ENCRYPT', false),
// Regenerate session on login
// App\Http\Controllers\Auth\AuthenticatedSessionController
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate(); // CRITICAL: prevents session fixation
return redirect()->intended(RouteServiceProvider::HOME);
}
// Invalidate session on logout
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}// App\Providers\AuthServiceProvider
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post): bool {
return $user->id === $post->user_id;
});
Gate::define('publish-post', function (User $user): bool {
return $user->role === 'editor' || $user->role === 'admin';
});
// Using before() for super-admin override
Gate::before(function (User $user, string $ability): ?bool {
if ($user->role === 'super-admin') {
return true; // Grants all abilities
}
return null; // Fall through to normal checks
});
}
// Usage in controllers
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update-post', $post);
// Or: $this->authorize('update-post', $post);
// Or: abort_unless(Auth::user()->can('update-post', $post), 403);
// ...
}// App\Policies\PostPolicy
class PostPolicy
{
use HandlesAuthorization;
public function viewAny(?User $user): bool
{
return true; // Public listing
}
public function view(?User $user, Post $post): bool
{
return $post->is_published || ($user && $user->id === $post->user_id);
}
public function create(User $user): bool
{
return $user->hasVerifiedEmail(); // Must verify email first
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id && $post->created_at->diffInDays(now()) <= 30;
}
public function restore(User $user, Post $post): bool
{
return $user->role === 'admin';
}
public function forceDelete(User $user, Post $post): bool
{
return $user->role === 'super-admin';
}
}
// Register in AuthServiceProvider
protected $policies = [
Post::class => PostPolicy::class,
];
// Controller usage
public function show(Post $post): View
{
$this->authorize('view', $post);
return view('posts.show', compact('post'));
}
// Blade usage
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
@cannot('update', $post)
<span>You cannot edit this post</span>
@endcannot// Using middleware in routes
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Route::get('/posts/create', [PostController::class, 'create'])
->middleware('can:create,App\Models\Post');
// Custom authorization middleware
// app/Http/Middleware/CheckRole.php
class CheckRole
{
public function handle(Request $request, Closure $next, string $role): mixed
{
if (!$request->user() || $request->user()->role !== $role) {
abort(403, 'Unauthorized. This area requires role: ' . $role);
}
return $next($request);
}
}
// Register in Kernel
protected $routeMiddleware = [
'role' => \App\Http\Middleware\CheckRole::class,
];
// Route usage
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin', [AdminController::class, 'index']);
});// BAD: $guarded = [] allows ALL columns to be mass-assigned
// NEVER use $guarded = [] in production
// GOOD: Whitelist fillable attributes
final class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'phone',
'avatar',
];
// NEVER add 'role', 'is_admin', 'is_verified' here
}
// GOOD: Explicitly control which fields can be filled in requests
public function store(StoreUserRequest $request): RedirectResponse
{
$user = User::create($request->safe()->only([
'name', 'email', 'phone', 'avatar'
]));
// $request->safe() uses validated data only
// $request->only() is NOT safe on its own without validation rules
}
// BAD: Creating a user with request data directly
User::create($request->all()); // VULNERABLE to mass assignment!
// BETTER: Use DTOs for creation
$user = User::create($request->validated()); // Only validated fields// GOOD: Eloquent automatically parameterizes queries
User::where('email', $userInput)->first();
User::whereRaw('email = ?', [$userInput])->first();
// GOOD: Query Builder also parameterizes
DB::table('users')->where('email', $userInput)->first();
DB::select('SELECT * FROM users WHERE email = ?', [$userInput]);
// BAD: Raw string interpolation
DB::select("SELECT * FROM users WHERE email = '{$userInput}'"); // VULNERABLE!
User::whereRaw("email = '{$userInput}'")->first(); // VULNERABLE!
// BAD: whereRaw/orderByRaw with unescaped input
User::orderByRaw($userInput); // VULNERABLE!
User::groupByRaw($userInput); // VULNERABLE!
// BAD: DB::statement with concatenation
DB::statement("INSERT INTO users (email) VALUES ('{$userInput}')"); // VULNERABLE!final class User extends Authenticatable
{
protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean', // Cast to boolean prevents string injection
'settings' => 'array', // Automatically json_encode/json_decode
'metadata' => 'encrypted:array', // Laravel 11+ encrypted casting
'password' => 'hashed', // Laravel 10+ auto-hashes on set
];
}final class User extends Authenticatable
{
// Hide sensitive attributes from JSON/API responses
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
// Append only safe computed attributes
protected $appends = ['full_name']; // safe
// NEVER append sensitive computed data
}
final class Post extends Model
{
// Global scope to filter soft deleted records
use SoftDeletes;
// Prevent N+1 by restricting lazy loading (optional strict mode)
// AppServiceProvider::boot()
// Model::preventLazyLoading(!app()->isProduction());
}// Laravel CSRF is enabled by default via VerifyCsrfToken middleware
// app/Http/Kernel.php (protected $middlewareGroups['web'])
// All POST/PUT/PATCH/DELETE forms must include @csrf
<form method="POST" action="/posts">
@csrf
<input type="text" name="title">
<button type="submit">Create</button>
</form>// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
// Only exclude routes that have external CSRF protection (webhooks, etc.)
protected $except = [
'stripe/*', // Stripe webhooks use their own signature verification
// Avoid blanket 'api/*' — stateful Sanctum routes need CSRF.
// Exclude only specific stateless webhook/endpoint routes.
];
}<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
// Axios example (Laravel ships with Axios)
axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector(
'meta[name="csrf-token"]'
).getAttribute('content');
// Fetch example
fetch('/posts', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
</script>{{-- SAFE: Auto-escaped by Blade --}}
{{ $userInput }}
{{-- DANGEROUS: Raw output — NEVER use with user input --}}
{!! $userInput !!}
{{-- SAFE: Only use {!! !!} with trusted content you control --}}
{!! $trustedHtmlFromYourServer !!}
{{-- GOOD: Use specific escaping directives --}}
@js($data) {{-- JSON encode for JavaScript --}}
@json($data) {{-- JSON encode in templates --}}
{{-- BAD: Direct user input in raw HTML --}}
<div>{!! $user->bio !!}</div> {{-- VULNERABLE if user provides bio --}}// When you must allow some HTML, use a whitelist approach
use HTMLPurifier; // Requires: composer require ezyang/htmlpurifier
public function sanitizeHtml(string $dirty): string
{
$config = \HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,i,a[href],ul,ol,li,br');
$config->set('URI.AllowedSchemes', ['http', 'https', 'mailto']);
$purifier = new \HTMLPurifier($config);
return $purifier->purify($dirty);
}
// In blade:
<div>{!! $sanitizedContent !!}</div> {{-- Safe after purification --}}{{-- SAFE: Blade @js escapes for JavaScript context --}}
<script>
const user = @js($user); // JSON + escaped for JS context
const settings = @json($settings); // Direct JSON encode
</script>
{{-- DANGEROUS: Manual JSON in JS context --}}
<script>
const user = {{ json_encode($user) }}; // NOT escaped for JS!
</script>// App\Http\Middleware\SecurityHeaders.php
class SecurityHeaders
{
public function handle(Request $request, Closure $next): mixed
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'"
);
return $response;
}
}
// Register in kernel
protected $middleware = [
\App\Http\Middleware\SecurityHeaders::class,
];final class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can('create', Post::class) ?? false;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255', 'sanitize_html'],
'content' => ['required', 'string', 'max:10000'],
'image' => [
'required',
'image',
'mimes:jpg,jpeg,png,gif,webp', // Whitelist specific types
'max:2048', // 2MB max
],
'tags' => ['array'],
'tags.*' => ['integer', 'exists:tags,id'],
];
}
public function messages(): array
{
return [
'title.max' => 'Post title must not exceed 255 characters.',
'image.max' => 'Image must be under 2MB.',
];
}
// Sanitize input after validation
public function validated($key = null, $default = null): mixed
{
$validated = parent::validated();
$validated['title'] = strip_tags($validated['title']);
return $key ? ($validated[$key] ?? $default) : $validated;
}
}// app/Rules/StrongPassword.php
class StrongPassword implements Rule
{
public function passes($attribute, $value): bool
{
return preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#^()_\-+=])[A-Za-z\d@$!%*?&#^()_\-+=]{12,}$/', $value);
}
public function message(): string
{
return 'The :attribute must be at least 12 characters with uppercase, lowercase, number, and symbol.';
}
}
// app/Rules/NotBlacklistedDomain.php
class NotBlacklistedDomain implements Rule
{
private array $blacklisted = ['mailinator.com', 'guerrillamail.com'];
public function passes($attribute, $value): bool
{
$domain = substr(strrchr($value, '@'), 1);
return !in_array(strtolower($domain), $this->blacklisted);
}
public function message(): string
{
return 'Email from disposable domains is not allowed.';
}
}// App/Providers/RouteServiceProvider
protected function configureRateLimiting(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('auth', function (Request $request) {
return Limit::perMinute(5)->by($request->ip())
->response(function () {
return response()->json([
'message' => 'Too many login attempts. Try again in 1 minute.',
], 429);
});
});
RateLimiter::for('uploads', function (Request $request) {
return Limit::perHour(10)->by($request->user()?->id ?? $request->ip())
->response(function () {
return response()->json([
'message' => 'Upload limit reached. Try again later.',
], 429);
});
});
}
// Route usage
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
Route::apiResource('posts', PostController::class);
});
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:auth');// Sanctum (recommended for most apps — simple, first-party, SPA)
// config/sanctum.php
'expiration' => 60 * 24, // Tokens expire after 24 hours
'model' => User::class,
// Issuing scoped tokens
$token = $user->createToken('client-name', [
'posts:read',
'posts:write',
])->plainTextToken;
// Middleware scoping
Route::middleware('auth:sanctum')->group(function () {
Route::get('/posts', [PostController::class, 'index'])
->middleware('abilities:posts:read');
Route::post('/posts', [PostController::class, 'store'])
->middleware('abilities:posts:write');
});
// Passport (OAuth2 — for third-party clients or complex auth flows)
// Install: composer require laravel/passport
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', '')), // Whitelist specific origins
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => ['X-Total-Count', 'X-Pagination-Page'],
'max_age' => 0,
'supports_credentials' => true, // Required for Sanctum SPA auth
];
// NEVER: Allow all origins in production unless absolutely necessary
// 'allowed_origins' => ['*'], // Only for truly public APIspublic function rules(): array
{
return [
'document' => [
'required',
'file',
'mimes:pdf,doc,docx,xls,xlsx', // Whitelist specific MIME types
'max:10240', // 10MB
'extensions:pdf,doc,docx,xls,xlsx', // Verify extension matches MIME
],
'avatar' => [
'nullable',
'image', // Ensures it's a valid image
'mimes:jpg,jpeg,png,webp',
'max:2048',
'dimensions:min_width=100,min_height=100,max_width=2000,max_height=2000',
],
];
}// Store sensitive files on the same private disk used to create temporary URLs.
$path = $request->file('document')->store('documents', 's3');
// Never use the 'public' disk for sensitive documents.
// Use signed URLs for temporary file access
use Illuminate\Support\Facades\Storage;
public function download(Document $document)
{
// Requires route-model binding for Document, a persisted $document->path,
// and a DocumentPolicy::download rule registered with Laravel.
// Authorize against the model that owns the file, never a path taken from the request
$this->authorize('download', $document);
// Generate the temporary signed URL (expires in 15 minutes) only after authorization
$url = Storage::disk('s3')->temporaryUrl($document->path, now()->addMinutes(15));
return redirect($url);
}
// Storage configuration for cloud with encryption
// config/filesystems.php
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'server_side_encryption' => 'AES256', // Encrypt at rest
],# Always audit dependencies in CI
composer audit
# Pin major versions in composer.json
"laravel/framework": "^11.0",
"spatie/laravel-permission": "^6.0"
# Check for abandoned packages
composer why-not
# Keep lock file in version control (it pins exact versions)
# Run `composer update` deliberately, never in CI/CD# .env file (NEVER commit)
# .gitignore includes .env by default
APP_KEY=base64:abc123...
DB_PASSWORD=secure_password
STRIPE_KEY=sk_live_...
SANCTUM_TOKEN_PREFIX=myapp_
# For production: Use a secret manager
# Deploy with: env $(aws secretsmanager get-secret-value --secret-id prod/db | jq ...) php artisan serve
# Validate secrets at boot (AppServiceProvider::boot)
$secrets = ['services.stripe.key', 'services.stripe.webhook_secret'];
foreach ($secrets as $key) {
if (empty(config($key))) {
Log::critical("Missing secret: {$key}");
}
}// Define a named rate limiter (typically in AppServiceProvider::boot())
RateLimiter::for('payments', fn () => Limit::perMinute(5));// Encrypt sensitive job data by implementing the interface
final class ProcessPaymentJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private readonly string $paymentIntentId, // Public IDs are fine
private readonly string $cardFingerprint, // Encrypted via ShouldBeEncrypted
) {}
public function handle(): void
{
// Process payment
}
// Limit retries and delay between attempts
public function retryUntil(): Carbon
{
return now()->addMinutes(5);
}
// Rate limit how many jobs of this type can run
public function middleware(): array
{
return [
new RateLimited('payments'),
];
}
}// config/logging.php
'channels' => [
'security' => [
'driver' => 'single',
'path' => storage_path('logs/security.log'),
'level' => 'warning',
],
],
// Audit log helper
final class SecurityLogger
{
public static function log(string $event, array $context = []): void
{
Log::channel('security')->warning($event, array_merge([
'user_id' => Auth::id(),
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
'url' => request()->fullUrl(),
'timestamp' => now()->toIso8601String(),
], $context));
}
}
// Usage
SecurityLogger::log('failed_login_attempt', ['email' => $email]);
SecurityLogger::log('password_change');
SecurityLogger::log('role_change', ['target_user' => $targetId, 'new_role' => 'admin']);
SecurityLogger::log('suspicious_activity', ['reason' => 'multiple_attempts_from_different_ips']);| Check | Description |
|---|---|
APP_DEBUG=false | Never run with debug enabled in production |
APP_KEY set | Always run php artisan key:generate |
| HTTPS enforced | Force HTTPS in production via middleware or proxy |
$fillable whitelisted | Never use $guarded = [] |
| CSRF active | @csrf on all state-changing forms |
| Sanctum/Passport configured | API authentication with token abilities/scopes |
| Rate limiting applied | Throttle API and auth endpoints |
| Input validation | FormRequest with specific rules, never $request->all() |
| File upload restrictions | Validate MIME types, size, dimensions |
composer audit in CI | Check dependencies for known vulnerabilities |
password_hash / password_verify | Use Laravel's built-in hashing (bcrypt/Argon2) |
| Session regeneration on login | Call $request->session()->regenerate() |
| Security headers middleware | CSP, X-Frame-Options, X-Content-Type-Options |
| Logged security events | Audit log for auth failures, role changes, suspicious activity |
.env not committed | Verify .gitignore includes .env |
laravel-patterns — Laravel architecture, routing, Eloquent, and API patternsbackend-patterns — General backend API and database patternslaravel-tdd — Laravel testing with PHPUnit and Pest© affaan-m, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/laravel-security of affaan-m/ECC.
Open the folder on GitHubat commit 2d515e4
We found 2 copies of this SKILL.md (exact, near-identical or edited) in other folders, from 1 other GitHub owner. This page covers the copy in affaan-m/ECC, which our catalogue first saw on October 7, 2026.
Laravel Security next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Laravel Security this skillaffaan-m/ECC | 277k | 1 repos | ~6.7k | Automated safety check: Notes | MIT | |
| Spa Auth Developmentgdarko/laravel-vue-starter | 145 | — | ~668 | Automated safety check: Notes | MIT | |
| Fortify Developmentcoollabsio/coolify | 63k | 4 repos | ~1.9k | Automated safety check: Pass | MIT | |
| Laravel SpecialistJeffallan/claude-skills | 12k | 1 repos | ~2.1k | Automated safety check: Pass | MIT | |
| Cashier Stripe Developmentluadotsh/lua | 343 | 1 repos | ~1.2k | Automated safety check: Pass | MIT | |
| Socialite Developmenthexlet-volunteers/hexlet-sicp | 114 | 5 repos | ~1.2k | Automated safety check: Pass | MIT |
gdarko/laravel-vue-starter
Activate when working on SPA authentication flow, Sanctum cookie-based auth, Vue Router guards, auth store, login/register/password reset pages, or CORS/session configuration.
coollabsio/coolify
ACTIVATE when the user works on authentication in Laravel. An agent skill from coollabsio/coolify.
Jeffallan/claude-skills
Builds Laravel 10+ applications with Eloquent models, Sanctum authentication, Horizon queues, API resources and Livewire components, tested with Pest or PHPUnit.
luadotsh/lua
Handles Laravel Cashier Stripe integration including subscriptions, webhooks, Stripe Checkout, invoices, charges, refunds, trials, coupons, metered billing, and payment failure handling.
hexlet-volunteers/hexlet-sicp
Manages OAuth social authentication with Laravel Socialite. An agent skill from hexlet-volunteers/hexlet-sicp.
trypostit/trypost
Develops OAuth2 API authentication with Laravel Passport. An agent skill from trypostit/trypost.
affaan-m/ECC
Audits your installed Claude skills and commands for quality, with a quick mode for recently changed skills and a full mode that evaluates all of them through subagents.
affaan-m/ECC
Ingests, indexes, searches, edits and monitors video, audio and live streams through the VideoDB Python SDK, returning stream links, clips and timestamps.
affaan-m/ECC
Route broad documentation-governance requests to existing ECC skills and run an opt-in, read-only audit of mapped documentation roles, links, ADR indexes, and evidence references.
affaan-m/ECC
Scans installed skills for principles that recur across them and proposes rule-file changes: append, revise, add a section, create a file or leave as covered.
affaan-m/ECC
Builds DRAFT counterparty agreements from one markdown template and a small JSON spec per party, with clauses picked by the party's role.
affaan-m/ECC
Set an ECC-specific frontend design direction for production UI work.
Works with
Categories
Laravel security best practices — authentication, authorization, Eloquent safety, CSRF, XSS prevention, API security, and secure deployment configurations. Laravel Security is an agent skill from affaan-m/ECC. Laravel security best practices — authentication, authorization, Eloquent safety, CSRF, XSS prevention, API security, and secure deployment configurations.
Laravel Security fits situations like: reviewing Laravel auth; eloquent safety; deployment configuration.
Run `npx skills add affaan-m/ECC --skill laravel-security -a claude-code`. Or copy the skill folder (skills/laravel-security in affaan-m/ECC) into .claude/skills/laravel-security in your project. Claude Code loads it when a task matches its description.
Run `npx skills add affaan-m/ECC --skill laravel-security -a codex`. Or copy the skill folder (skills/laravel-security in affaan-m/ECC) into .agents/skills/laravel-security in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add affaan-m/ECC --skill laravel-security -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/laravel-security, .gemini/skills/laravel-security, .github/skills/laravel-security and .opencode/skills/laravel-security in your project.
Going by SKILL.md and its folder, Laravel Security needs the command-line tools its instructions call (composer and php) and credentials named APP_KEY, DB_PASSWORD, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. Our summary lists: A credential in APP_KEY.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.
Laravel Security is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 6.7k tokens (SKILL.md is roughly 27k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Laravel Security: Spa Auth Development (gdarko/laravel-vue-starter, 145 stars), Fortify Development (coollabsio/coolify, 63k stars), Laravel Specialist (Jeffallan/claude-skills, 12k stars) and Cashier Stripe Development (luadotsh/lua, 343 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
affaan-m (a GitHub user) maintains it in affaan-m/ECC, which has 276,673 GitHub stars. The repository holds 683 skills in this directory. The repository was last updated on October 11, 2026.
Source: affaan-m/ECC on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.