juststeveking/laravel-bastion
Stripe-inspired API authentication with environment isolation, granular scopes, and built-in security.
Laravel Bastion
Stripe-inspired API authentication with environment isolation, granular scopes, and built-in security.
Features
- 🔐 Stripe-style API Tokens - Prefixed tokens with environment indicators (
app_test_pk_*,app_live_sk_*) - 🌍 Environment Isolation - Separate test and live environments with automatic validation
- 🎯 Granular Scopes - Fine-grained permission control with wildcard support
- 🔑 Token Types - Public, Secret, and Restricted keys with different access levels
- 📝 Audit Logging - Comprehensive activity tracking for compliance and debugging
- 🪝 Webhook Support - Built-in webhook endpoints with signature verification
- 🛡️ Security First - Expiration dates and secure token hashing
- ⚡ Laravel Native - Built with Laravel conventions and best practices
Requirements
- PHP 8.4 or higher
- Laravel 12.x
Installation
Install the package via Composer:
composer require juststeveking/laravel-bastionRun the installation command:
php artisan bastion:installThis will:
- Publish the configuration file to
config/bastion.php - Publish the database migrations
- Optionally run the migrations
Add the Trait to Your User Model
use JustSteveKing\Bastion\Concerns\HasBastionTokens;
class User extends Authenticatable{ use HasBastionTokens;
// ...}Quick Start
Generate a Token
use JustSteveKing\Bastion\Enums\TokenEnvironment;use JustSteveKing\Bastion\Enums\TokenType;
$result = $user->createBastionToken( name: 'My API Key', scopes: ['users:read', 'users:write'], environment: TokenEnvironment::Test, type: TokenType::Restricted,);
// Store this securely - it's only shown once!$token = $result['plainTextToken'];// Example: app_test_rk_a8Kx7mN2pQ4vW9yB1cD3eF5gH6jK8lM
echo "Token: " . $token;Protect Routes with Middleware
use JustSteveKing\Bastion\Http\Middleware\AuthenticateToken;
Route::middleware(AuthenticateToken::class)->group(function () { Route::get('/api/users', [UserController::class, 'index']);});
// Require specific scopeRoute::middleware([AuthenticateToken::class . ':users:write']) ->post('/api/users', [UserController::class, 'store']);Make Authenticated Requests
curl -H "Authorization: Bearer app_test_rk_..." \ https://your-api.com/api/usersToken Types
Bastion supports three token types, inspired by Stripe:
Public Keys (pk)
TokenType::Public- Prefix:
app_{env}_pk_* - Limited access, safe for client-side use
- Ideal for JavaScript/mobile apps
- Cannot perform sensitive operations
Secret Keys (sk)
TokenType::Secret- Prefix:
app_{env}_sk_* - Full access to all permitted scopes
- Must be kept secure on the server
- Use for backend integrations
Restricted Keys (rk)
TokenType::Restricted- Prefix:
app_{env}_rk_* - Scoped access with specific permissions
- Best for third-party integrations
- Follows principle of least privilege
Environments
Bastion isolates test and production data:
Test Environment
TokenEnvironment::Test- For development and testing
- Higher rate limits (default: 100/min)
- Can be used in any environment
Live Environment
TokenEnvironment::Live- For production traffic
- Standard rate limits (default: 60/min)
- Can be restricted from non-production environments (configurable)
Advanced Features
Token Rotation
Rotate tokens to create a new token while revoking the old one:
$result = $token->rotate();
// Get the new token (store securely)$newToken = $result['plainTextToken'];$newTokenModel = $result['token'];
// The old token is automatically revokedYou can also rotate via CLI:
php artisan bastion:rotate {token-id}Scopes and Permissions
Bastion uses a flexible scope system with wildcard support:
// Grant specific permissions$user->createBastionToken( name: 'User Manager', scopes: ['users:read', 'users:write'],);
// Use wildcards for category-level access$user->createBastionToken( name: 'Payment API', scopes: ['payments:*'], // All payment operations);
// Full access$user->createBastionToken( name: 'Admin Token', scopes: ['*'], // All scopes);Built-in Scope Examples
The package includes example scopes in ApiScope enum:
users:read,users:write,users:deletepayments:read,payments:create,payments:refundwebhooks:read,webhooks:write*(admin/full access)
You can define your own scopes - they’re just strings following the resource:action pattern.
Webhooks
Create webhook endpoints to receive real-time notifications:
use JustSteveKing\Bastion\Models\WebhookEndpoint;
$result = WebhookEndpoint::createEndpoint([ 'user_id' => $user->id, 'url' => 'https://your-app.com/webhooks/bastion', 'events' => ['token.created', 'token.revoked', 'token.used'], 'environment' => TokenEnvironment::Live, 'is_active' => true,]);
// Store the signing secret securely!$signingSecret = $result['signingSecret'];// Example: whsec_a8Kx7mN2pQ4vW9yB1cD3eF5gH6jK8lMVerifying Webhook Signatures
use JustSteveKing\Bastion\Models\WebhookEndpoint;
Route::post('/webhooks/bastion', function (Request $request) { $endpoint = WebhookEndpoint::where('secret_prefix', '...')->first();
$signature = $request->header('X-Bastion-Signature'); $timestamp = $request->header('X-Bastion-Timestamp'); $payload = $request->getContent();
if (!$endpoint->verifySignature($payload, $signature, (int)$timestamp)) { abort(401, 'Invalid signature'); }
// Process webhook... $event = $request->input('event'); $data = $request->input('data');
return response()->json(['received' => true]);});Events
Bastion dispatches events for all token lifecycle actions:
use JustSteveKing\Bastion\Events\{ TokenCreated, TokenUsed, TokenRevoked, TokenRotated, TokenExpired};
// Listen to events in your EventServiceProviderEvent::listen(TokenCreated::class, function (TokenCreated $event) { // $event->token - The BastionToken model // $event->plainTextToken - The plain text token (only in TokenCreated) Log::info('Token created', ['token_id' => $event->token->id]);});
Event::listen(TokenUsed::class, function (TokenUsed $event) { // $event->token // $event->ipAddress // $event->userAgent // $event->endpoint});
Event::listen(TokenRevoked::class, function (TokenRevoked $event) { // $event->token // $event->reason Mail::to($event->token->user)->send(new TokenRevokedNotification($event));});Audit Logging
Enable comprehensive API request auditing by adding the middleware:
use JustSteveKing\Bastion\Http\Middleware\{AuthenticateToken, AuditApiRequest};
Route::middleware([AuthenticateToken::class, AuditApiRequest::class]) ->group(function () { // All requests will be logged Route::get('/api/users', [UserController::class, 'index']); });Audit logs capture:
- Request method, path, and query parameters
- Response status code and time
- IP address and user agent
- Token and user information
- Request/response bodies (configurable)
Query audit logs:
use JustSteveKing\Bastion\Models\AuditLog;
// Get recent activity for a token$logs = AuditLog::where('bastion_token_id', $token->id) ->latest() ->take(100) ->get();
// Find failed requests$failures = AuditLog::where('status_code', '>=', 400) ->where('created_at', '>=', now()->subDay()) ->get();CLI Commands
Bastion provides several Artisan commands for token management:
Generate Token
php artisan bastion:generate {user-id} "Token Name" \ --environment=test \ --type=restricted \ --scopes=users:read --scopes=users:writeRevoke Token
# Revoke by token IDphp artisan bastion:revoke 123 --reason="Security incident"
# Revoke by token prefixphp artisan bastion:revoke abc12345 --reason="No longer needed"
# Revoke all tokens for a userphp artisan bastion:revoke 0 --all-user=456 --reason="User offboarded"Rotate Token
php artisan bastion:rotate {token-id}Prune Expired Tokens
# Prune expired tokensphp artisan bastion:prune-tokens --expired
# Prune tokens unused for 90 daysphp artisan bastion:prune-tokens --days=90Prune Old Audit Logs
# Use config default (90 days)php artisan bastion:prune-logs
# Custom retention periodphp artisan bastion:prune-logs --days=30Schedule these commands in your app/Console/Kernel.php:
protected function schedule(Schedule $schedule): void{ $schedule->command('bastion:prune-tokens --expired')->daily(); $schedule->command('bastion:prune-logs')->weekly();}Configuration
Publish and edit the configuration file:
php artisan vendor:publish --tag=bastion-configKey Configuration Options
return [ // Table names (customizable) 'tables' => [ 'tokens' => 'bastion_tokens', 'audit_logs' => 'bastion_audit_logs', 'webhooks' => 'bastion_webhook_endpoints', ],
// Token expiration (days) 'token_expiration_days' => null,
// Audit log retention (days) 'audit_log_retention_days' => 90,
// Rate limits per minute 'rate_limits' => [ 'test' => 100, 'live' => 60, ],
// Security settings 'security' => [ 'prevent_test_tokens_in_production' => true, 'enable_audit_logging' => true, 'enable_alerting' => true, ],
// Error response format 'errors' => [ 'use_rfc7807' => true, // RFC 7807 Problem Details 'base_url' => 'https://bastion.laravel.com/errors/', // Base for problem type URLs ],
// User model 'user_model' => App\Models\User::class,];RFC 7807 Base URL
Bastion returns errors in RFC 7807 Problem Details format by default. You can customize the base URL used for the type field in error responses:
'errors' => [ 'use_rfc7807' => true, 'base_url' => 'https://bastion.laravel.com/errors/',],With this configuration, an unauthenticated request will return a type like:
https://bastion.laravel.com/errors/token_missinghttps://bastion.laravel.com/errors/token_invalidhttps://bastion.laravel.com/errors/insufficient_scope
Adjust base_url to point to your own error documentation if desired.
Security Best Practices
- Never log tokens - Only the HMAC hash is stored in the database
- Show tokens once - Display the plain text token only at creation time
- Use HTTPS exclusively - Always transmit tokens over encrypted connections
- Use restricted tokens - Grant minimum necessary permissions (principle of least privilege)
- Set expiration dates - Especially for temporary integrations
- Rotate tokens regularly - Implement a token rotation policy (e.g., every 90 days)
- Monitor audit logs - Watch for suspicious activity and unusual patterns
- Use test tokens in development - Keep live tokens in production only
- Store tokens securely - Use environment variables or secure vaults (AWS Secrets Manager, HashiCorp Vault)
Token Security Features
Laravel Bastion implements multiple security layers:
- HMAC-SHA256 hashing - Tokens are hashed with your application key
- Constant-time comparison - Prevents timing attacks during token lookup
- Cryptographically secure RNG - Uses
random_bytes()for token generation - Environment isolation - Prevents test tokens in production (configurable)
- Automatic event dispatching - Monitor all token lifecycle events
Community Requests
Have a feature idea? Open an issue with the enhancement label.
Out of Scope
Bastion focuses on token-based authentication with scopes and environments. It does not implement:
- IP allowlisting or CIDR-based restrictions
- Domain/host origin restrictions
If you need these controls, add them at your application layer (e.g., trusted proxies, firewall/WAF rules, or custom middleware) alongside Bastion.