juststeveking / laravel-bastion
Stripe-inspired API authentication with environment isolation, granular scopes, and built-in security.
Fund package maintenance!
juststeveking
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 14
Watchers: 0
Forks: 1
pkg:composer/juststeveking/laravel-bastion
Requires
- php: ^8.4
Requires (Dev)
- laravel/pint: ^1.21
- orchestra/testbench: ^9.11
- phpstan/phpstan: ^2.1
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.5.8
- roave/security-advisories: dev-latest
This package is auto-updated.
Last update: 2025-10-10 07:20:41 UTC
README
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-bastion
Run the installation command:
php artisan bastion:install
This 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 scope Route::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/users
Token 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 revoked
You 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:delete
payments:read
,payments:create
,payments:refund
webhooks: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_a8Kx7mN2pQ4vW9yB1cD3eF5gH6jK8lM
Verifying 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 EventServiceProvider Event::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:write
Revoke Token
# Revoke by token ID php artisan bastion:revoke 123 --reason="Security incident" # Revoke by token prefix php artisan bastion:revoke abc12345 --reason="No longer needed" # Revoke all tokens for a user php artisan bastion:revoke 0 --all-user=456 --reason="User offboarded"
Rotate Token
php artisan bastion:rotate {token-id}
Prune Expired Tokens
# Prune expired tokens php artisan bastion:prune-tokens --expired # Prune tokens unused for 90 days php artisan bastion:prune-tokens --days=90
Prune Old Audit Logs
# Use config default (90 days) php artisan bastion:prune-logs # Custom retention period php artisan bastion:prune-logs --days=30
Schedule 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-config
Key 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:
// config/bastion.php '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_missing
https://bastion.laravel.com/errors/token_invalid
https://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.