Security Guide
This guide explains security best practices for Laravel Spectrum and APIs.
🔐 Authentication and Authorization
Implementing Authentication
// config/auth.php
'guards' => [
'api' => [
'driver' => 'sanctum',
'provider' => 'users',
],
'api-jwt' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
// Protecting routes
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('users', UserController::class);
});
Two-Factor Authentication (2FA)
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Enable2FARequest;
use App\Http\Requests\Verify2FARequest;
use PragmaRX\Google2FA\Google2FA;
class TwoFactorAuthController extends Controller
{
protected $google2fa;
public function __construct(Google2FA $google2fa)
{
$this->google2fa = $google2fa;
}
public function enable(Enable2FARequest $request)
{
$user = $request->user();
// Generate secret key
$secretKey = $this->google2fa->generateSecretKey();
// Generate QR code URL
$qrCodeUrl = $this->google2fa->getQRCodeUrl(
config('app.name'),
$user->email,
$secretKey
);
// Temporarily save
$user->two_factor_secret = encrypt($secretKey);
$user->save();
return response()->json([
'secret' => $secretKey,
'qr_code' => $qrCodeUrl,
]);
}
public function verify(Verify2FARequest $request)
{
$user = $request->user();
$secret = decrypt($user->two_factor_secret);
$valid = $this->google2fa->verifyKey($secret, $request->code);
if (!$valid) {
return response()->json([
'message' => 'Invalid authentication code',
], 422);
}
$user->two_factor_enabled = true;
$user->save();
// Generate recovery codes
$recoveryCodes = $this->generateRecoveryCodes();
$user->two_factor_recovery_codes = encrypt($recoveryCodes);
$user->save();
return response()->json([
'message' => 'Two-factor authentication has been enabled',
'recovery_codes' => $recoveryCodes,
]);
}
}
🛡️ API Security
Rate Limiting
// app/Http/Kernel.php
protected $middlewareGroups = [
'api' => [
'throttle:60,1', // 60 requests per minute
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
// Custom rate limiting
Route::middleware('throttle:custom')->group(function () {
Route::post('/api/expensive-operation', ExpensiveController::class);
});
// RateLimiter configuration
// app/Providers/RouteServiceProvider.php
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('custom', function (Request $request) {
return Limit::perHour(10)
->by($request->user()?->id ?: $request->ip())
->response(function (Request $request, array $headers) {
return response()->json([
'message' => 'Request limit reached',
'retry_after' => $headers['Retry-After'],
], 429);
});
});
}
CORS Configuration
// config/cors.php
return [
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => [
env('FRONTEND_URL', 'http://localhost:3000'),
'https://app.example.com',
],
'allowed_origins_patterns' => [
'#^https://.*\.example\.com$#',
],
'allowed_headers' => ['*'],
'exposed_headers' => [
'X-Request-ID',
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
],
'max_age' => 86400,
'supports_credentials' => true,
];
🔍 Input Validation and Sanitization
Strict Input Validation
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class CreateUserRequest extends FormRequest
{
public function rules()
{
return [
'name' => [
'required',
'string',
'max:255',
'regex:/^[a-zA-Z\s\-]+$/', // Letters, spaces, and hyphens only
],
'email' => [
'required',
'email:rfc,dns', // Also perform DNS validation
'unique:users,email',
'max:255',
],
'password' => [
'required',
'string',
'min:12', // Minimum 12 characters
'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/', // Complexity requirements
'confirmed',
'not_compromised', // Have I Been Pwned API check
],
'phone' => [
'nullable',
'string',
'regex:/^\+?[1-9]\d{1,14}$/', // E.164 format
],
'role' => [
'required',
Rule::in(['user', 'admin', 'moderator']),
],
];
}
public function messages()
{
return [
'password.regex' => 'Password must contain uppercase, lowercase, numbers, and special characters',
'password.not_compromised' => 'This password has been found in a data breach',
];
}
protected function prepareForValidation()
{
// Input sanitization
$this->merge([
'name' => strip_tags($this->name),
'email' => strtolower(trim($this->email)),
'phone' => preg_replace('/[^0-9+]/', '', $this->phone),
]);
}
}
SQL Injection Prevention
// Bad example - SQL injection vulnerability
$users = DB::select("SELECT * FROM users WHERE email = '{$request->email}'");
// Good example - Parameter binding
$users = DB::select('SELECT * FROM users WHERE email = ?', [$request->email]);
// Using Eloquent (recommended)
$users = User::where('email', $request->email)->get();
// For complex queries
$results = DB::table('orders')
->whereRaw('created_at > ?', [$date])
->whereRaw('total > ?', [$amount])
->get();
🚨 Security Headers
Security Middleware
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class SecurityHeaders
{
public function handle(Request $request, Closure $next)
{
$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('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// Content Security Policy
$response->headers->set('Content-Security-Policy', implode('; ', [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
]));
// Strict Transport Security (HTTPS only)
if ($request->secure()) {
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
return $response;
}
}
🔑 API Key Management
API Key Implementation
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class ApiKey extends Model
{
protected $fillable = [
'name',
'key',
'secret',
'permissions',
'rate_limit',
'expires_at',
'last_used_at',
'last_ip_address',
];
protected $casts = [
'permissions' => 'array',
'expires_at' => 'datetime',
'last_used_at' => 'datetime',
];
protected $hidden = [
'secret',
];
public static function generate(string $name, array $permissions = []): self
{
$key = 'pk_' . Str::random(32);
$secret = 'sk_' . Str::random(32);
return static::create([
'name' => $name,
'key' => $key,
'secret' => hash('sha256', $secret),
'permissions' => $permissions,
'rate_limit' => 1000, // Default: 1000 requests per hour
]);
}
public function validateSecret(string $secret): bool
{
return hash('sha256', $secret) === $this->secret;
}
public function hasPermission(string $permission): bool
{
return in_array($permission, $this->permissions ?? []) ||
in_array('*', $this->permissions ?? []);
}
public function isExpired(): bool
{
return $this->expires_at && $this->expires_at->isPast();
}
}
API Key Authentication Middleware
namespace App\Http\Middleware;
use App\Models\ApiKey;
use Closure;
use Illuminate\Http\Request;
class AuthenticateApiKey
{
public function handle(Request $request, Closure $next, string $permission = null)
{
$key = $request->header('X-API-Key');
$secret = $request->header('X-API-Secret');
if (!$key || !$secret) {
return response()->json([
'error' => 'API key required',
], 401);
}
$apiKey = ApiKey::where('key', $key)->first();
if (!$apiKey || !$apiKey->validateSecret($secret)) {
return response()->json([
'error' => 'Invalid API key',
], 401);
}
if ($apiKey->isExpired()) {
return response()->json([
'error' => 'API key has expired',
], 401);
}
if ($permission && !$apiKey->hasPermission($permission)) {
return response()->json([
'error' => 'Insufficient permissions for this operation',
'required_permission' => $permission,
], 403);
}
// Update usage
$apiKey->update([
'last_used_at' => now(),
'last_ip_address' => $request->ip(),
]);
$request->merge(['api_key' => $apiKey]);
return $next($request);
}
}
📊 Audit Logging
Activity Log Implementation
namespace App\Traits;
use App\Models\AuditLog;
use Illuminate\Support\Facades\Auth;
trait Auditable
{
public static function bootAuditable()
{
static::created(function ($model) {
$model->logActivity('created');
});
static::updated(function ($model) {
$model->logActivity('updated', $model->getChanges());
});
static::deleted(function ($model) {
$model->logActivity('deleted');
});
}
public function logActivity(string $event, array $properties = [])
{
AuditLog::create([
'user_id' => Auth::id(),
'user_type' => Auth::user() ? get_class(Auth::user()) : null,
'event' => $event,
'auditable_type' => get_class($this),
'auditable_id' => $this->getKey(),
'old_values' => $event === 'updated' ? $this->getOriginal() : null,
'new_values' => $event === 'updated' ? $this->getAttributes() : null,
'properties' => $properties,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'performed_at' => now(),
]);
}
}
🔒 Data Encryption
Sensitive Data Encryption
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
class SensitiveData extends Model
{
protected $casts = [
'data' => 'encrypted',
'metadata' => 'encrypted:array',
];
// Custom encryption
public function setCustomDataAttribute($value)
{
$this->attributes['custom_data'] = encrypt($value, false);
}
public function getCustomDataAttribute($value)
{
return $value ? decrypt($value, false) : null;
}
// Hashing (searchable encryption)
public function setSsnAttribute($value)
{
$this->attributes['ssn'] = bcrypt($value);
$this->attributes['ssn_encrypted'] = encrypt($value);
}
public function verifySsn($value): bool
{
return password_verify($value, $this->ssn);
}
}
💡 Security Best Practices
1. Principle of Least Privilege
- Grant users only the minimum necessary permissions
- Set specific permissions for API keys
- Strictly manage administrator privileges
2. Security Updates
# Regular dependency updates
composer update --with-all-dependencies
npm audit fix
# Security vulnerability checks
composer audit
npm audit
3. Environment Variable Management
// .env.example
APP_DEBUG=false
APP_ENV=production
# Always use HTTPS in production
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=strict
📚 Related Documentation
- Authentication Configuration - Authentication system details
- Middleware - Security middleware
- Error Handling - Secure error handling