Security - v2.0.2
API Security Layer
The API Security Layer extends Laravel Sanctum with comprehensive token management, expiration, revocation tracking, and API-specific rate limiting.
Requirements
- Laravel Sanctum (
composer require laravel/sanctum) - Sanctum's
personal_access_tokensmigration must be run
Installation
1. Install Laravel Sanctum
If you haven't already, install Laravel Sanctum:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
2. Enable API Security
The API Security Layer is enabled by default. To disable it, set the environment variable:
SECURITY_API_ENABLED=false
Or in your configuration:
// config/artisanpack/security.php
'api' => [
'enabled' => false,
],
3. Add the Trait to Your User Model
use ArtisanPackUI\Security\Concerns\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
// ...
}
4. Run Migrations
The package will automatically add additional columns to the personal_access_tokens table for expiration and revocation tracking.
Configuration
The full configuration options are available in config/artisanpack/security.php under the api key:
'api' => [
'enabled' => env('SECURITY_API_ENABLED', true),
'driver' => 'sanctum',
'tokens' => [
'expiration' => env('API_TOKEN_EXPIRATION', 60 * 24 * 7), // 7 days
'prefix' => env('API_TOKEN_PREFIX', 'artisanpack'),
],
'abilities' => [
'read' => 'Read-only access to resources',
'write' => 'Create and update resources',
'delete' => 'Delete resources',
'admin' => 'Full administrative access',
],
'ability_groups' => [
'readonly' => ['read'],
'standard' => ['read', 'write'],
'full' => ['read', 'write', 'delete'],
'admin' => ['read', 'write', 'delete', 'admin'],
],
'rate_limiting' => [
'enabled' => env('API_RATE_LIMITING_ENABLED', true),
'authenticated' => [
'max_attempts' => env('API_RATE_LIMIT_AUTHENTICATED', 60),
'decay_minutes' => 1,
],
'guest' => [
'max_attempts' => env('API_RATE_LIMIT_GUEST', 30),
'decay_minutes' => 1,
],
'token_requests' => [
'max_attempts' => env('API_RATE_LIMIT_TOKEN', 5),
'decay_minutes' => 1,
],
],
],
Token Management
Creating Tokens Programmatically
// Create a token with default expiration
$token = $user->createApiToken('my-app-token');
echo $token->plainTextToken; // Use this for API authentication
// Create a token with specific abilities
$token = $user->createApiToken('read-only-token', ['read']);
// Create a token with custom expiration (30 days)
$token = $user->createApiToken('long-lived-token', ['*'], 60 * 24 * 30);
// Create a token with metadata
$token = $user->createApiToken('service-token', ['*'], null, [
'service' => 'ci-cd',
'environment' => 'production',
]);
// Create a token using an ability group
$token = $user->createApiTokenWithGroup('admin-token', 'admin');
Managing Tokens
// Get all active tokens for a user
$tokens = $user->activeApiTokens();
// Get all tokens (including expired and revoked)
$tokens = $user->allApiTokens();
// Revoke a specific token
$user->revokeApiToken($tokenId);
// Revoke all tokens
$user->revokeAllApiTokens();
// Revoke all tokens except the current one
$user->revokeOtherApiTokens();
// Delete expired tokens
$user->pruneExpiredApiTokens();
// Delete revoked tokens
$user->pruneRevokedApiTokens();
// Get token statistics
$stats = $user->apiTokenStats();
// Returns: ['total' => 5, 'active' => 3, 'expired' => 1, 'revoked' => 1, ...]
Token Model Methods
$token = ApiToken::find($id);
// Check status
$token->isValid(); // Not expired and not revoked
$token->isExpired(); // Past expiration date
$token->is_revoked; // Has been revoked
// Revoke the token
$token->revoke();
// Check abilities
$token->hasAbility('read');
$token->hasAllAbilities(['read', 'write']);
$token->hasAnyAbility(['admin', 'write']);
// Get human-readable status
$token->expiration_status; // "Active", "Expired 2 hours ago", "Revoked", etc.
// Metadata
$token->getMetadata('service');
$token->setMetadata('last_sync', now());
Middleware
API Security Middleware
Validates token expiration/revocation and records usage:
Route::middleware(['auth:sanctum', 'api.security'])->group(function () {
Route::get('/user', fn() => auth()->user());
});
Token Ability Middleware
Check if the token has ALL required abilities:
// Requires the 'write' ability
Route::middleware(['auth:sanctum', 'token.ability:write'])
->post('/posts', [PostController::class, 'store']);
// Requires BOTH 'read' and 'write' abilities
Route::middleware(['auth:sanctum', 'token.ability:read,write'])
->put('/posts/{id}', [PostController::class, 'update']);
Token Ability Any Middleware
Check if the token has ANY of the required abilities:
// Requires either 'admin' OR 'moderator' ability
Route::middleware(['auth:sanctum', 'token.ability.any:admin,moderator'])
->delete('/posts/{id}', [PostController::class, 'destroy']);
API Rate Limiting Middleware
Applies API-specific rate limits based on authentication status:
Route::middleware(['api.throttle'])->group(function () {
// Authenticated users: 60 requests/minute
// Guest users: 30 requests/minute
});
Combining Middleware
Route::middleware([
'auth:sanctum',
'api.security',
'api.throttle',
'token.ability:write',
])->group(function () {
Route::post('/posts', [PostController::class, 'store']);
Route::put('/posts/{id}', [PostController::class, 'update']);
});
Artisan Commands
Create a Token
# Create a token for user ID 1
php artisan api:token:create 1 --name="My Token"
# Create a token for user by email
php artisan api:token:create user@example.com --name="Service Token"
# Create a token with specific abilities
php artisan api:token:create 1 --name="Read Only" --abilities=read
# Create a token with multiple abilities
php artisan api:token:create 1 --name="Editor" --abilities=read --abilities=write
# Create a token using an ability group
php artisan api:token:create 1 --name="Admin Token" --group=admin
# Create a token with custom expiration (30 days)
php artisan api:token:create 1 --name="Long Token" --expires=43200
List Tokens
# List all tokens
php artisan api:token:list
# List tokens for a specific user
php artisan api:token:list 1
php artisan api:token:list user@example.com
# List only active tokens
php artisan api:token:list --active
# List only expired tokens
php artisan api:token:list --expired
# List only revoked tokens
php artisan api:token:list --revoked
Revoke Tokens
# Revoke a specific token by ID
php artisan api:token:revoke 123
# Revoke all tokens for a user
php artisan api:token:revoke --user=1 --all
# Revoke all expired tokens
php artisan api:token:revoke --expired
# Skip confirmation
php artisan api:token:revoke --expired --force
Prune (Delete) Tokens
# Delete tokens unused for 30 days (default)
php artisan api:token:prune
# Delete tokens unused for 7 days
php artisan api:token:prune --days=7
# Delete all expired tokens
php artisan api:token:prune --expired
# Delete all revoked tokens
php artisan api:token:prune --revoked
# Skip confirmation
php artisan api:token:prune --expired --force
Check Security Configuration
php artisan api:security:check
This command validates:
- Sanctum is installed
- Token expiration is configured
- Rate limiting is enabled
- HTTPS in production
Testing
Using the Test Trait
use ArtisanPackUI\Security\Testing\ApiSecurityAssertions;
class ApiTest extends TestCase
{
use ApiSecurityAssertions;
public function test_authenticated_user_can_access_api()
{
$user = User::factory()->create();
$response = $this->apiAs($user, 'GET', '/api/user');
$response->assertOk();
}
public function test_unauthenticated_request_returns_401()
{
$response = $this->getJson('/api/user');
$this->assertRequiresAuthentication($response);
}
public function test_token_without_ability_returns_403()
{
$user = User::factory()->create();
$response = $this->apiAs($user, 'POST', '/api/posts', [], ['read']);
$this->assertRequiresAbility($response);
}
public function test_expired_token_is_rejected()
{
$user = User::factory()->create();
$token = $this->createExpiredTestApiToken($user);
$response = $this->apiWithToken($token, 'GET', '/api/user');
$response->assertStatus(401);
}
}
Available Test Methods
// Create tokens
$token = $this->createTestApiToken($user, ['read', 'write']);
$token = $this->createExpiredTestApiToken($user);
$token = $this->createRevokedTestApiToken($user);
// Make authenticated requests
$response = $this->apiAs($user, 'GET', '/api/endpoint');
$response = $this->apiWithToken($token, 'POST', '/api/endpoint', $data);
// Assertions
$this->assertRequiresAuthentication($response);
$this->assertRequiresAbility($response);
$this->assertRateLimited($response);
$this->assertTokenValid($token);
$this->assertTokenRevoked($token);
$this->assertTokenExpired($token);
$this->assertTokenHasAbilities($token, ['read', 'write']);
Security Best Practices
Token Storage
- Never store tokens in plain text on the client
- Use secure storage mechanisms (Keychain, encrypted storage)
- Don't log tokens or include them in error messages
Token Rotation
Implement regular token rotation for long-lived integrations:
// Rotate token: create new, revoke old
$newToken = $user->createApiToken('service-token', $oldToken->abilities);
$oldToken->revoke();
Expiration Strategy
- Use short-lived tokens for user-facing applications
- Use longer expiration for service-to-service communication
- Consider implementing refresh tokens for mobile apps
Ability Scoping
- Follow the principle of least privilege
- Create specific abilities for specific actions
- Use ability groups for common permission sets
Rate Limiting
- Always enable rate limiting in production
- Use stricter limits for authentication endpoints
- Consider implementing per-endpoint rate limits
How It Integrates with Sanctum
This package extends Laravel Sanctum rather than replacing it:
- Custom Token Model: Our
ApiTokenmodel extends Sanctum'sPersonalAccessToken - Extended Trait:
HasApiTokenswraps Sanctum's trait with additional methods - Authentication Flow: Uses Sanctum's
auth:sanctumguard, then adds our middleware - Full Compatibility: Standard Sanctum usage still works
Request with Bearer Token
│
▼
┌─────────────────────┐
│ auth:sanctum │ ◄── Sanctum validates token
└─────────────────────┘
│
▼
┌─────────────────────┐
│ api.security │ ◄── Our middleware checks expiration/revocation
└─────────────────────┘
│
▼
┌─────────────────────┐
│ token.ability │ ◄── Our middleware checks abilities
└─────────────────────┘
│
▼
Controller