bspdx / keystone
Complete authentication package for Laravel with Fortify, Passkeys, TOTP 2FA, and RBAC
Installs: 5
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/bspdx/keystone
Requires
- php: ^8.2
- laravel/fortify: ^1.31
- laravel/framework: ^12.0
- laravel/sanctum: ^4.0
- pragmarx/google2fa-laravel: ^2.0
- spatie/laravel-passkeys: ^1.5
- spatie/laravel-permission: ^6.0
Requires (Dev)
- fakerphp/faker: ^1.23
- laravel/pail: ^1.2.2
- laravel/pint: ^1.24
- laravel/sail: ^1.41
- mockery/mockery: ^1.6
- nunomaduro/collision: ^8.6
- phpunit/phpunit: ^11.5.3
Replaces
- bspdx/authkit: 0.6.0
README
A comprehensive, production-ready authentication package for Laravel 12 with an API-first architecture. Keystone combines the power of Laravel Fortify, Sanctum, Spatie Laravel Permission, and Spatie Laravel Passkeys to provide a full-featured auth system with:
- 🔐 Standard Authentication - Powered by Laravel Fortify
- 👥 Role-Based Access Control (RBAC) - Clean service layer API
- 📱 TOTP Two-Factor Authentication - Google Authenticator, Authy, etc.
- 🔑 Passkey Authentication - Modern WebAuthn/FIDO2 login
- 🛡️ Passkey as 2FA - Use passkeys as a second factor
- 🎨 Optional Blade UI Components - Pre-built views for Laravel projects
- 🌐 API-First Design - Works with React, Vue, mobile apps, or any frontend
- 🏢 Multi-Tenancy Ready - Optional tenant scoping
Frontend Flexibility
Keystone works with any frontend framework:
- ✅ React, Vue, Angular, Svelte - Use the JSON API endpoints
- ✅ Mobile Apps - iOS, Android, React Native, Flutter
- ✅ Laravel Blade - Optional pre-built UI components included
- ✅ Inertia.js - Perfect for hybrid approaches
All controllers return JSON when requested, making Keystone truly framework-agnostic at the API level.
Table of Contents
- Frontend Flexibility
- Requirements
- Installation
- Configuration
- Usage
- Architecture
- HTTPS Setup
- Multi-Tenancy
- Testing
- Credits
- License
Requirements
- PHP 8.2+
- Laravel 12.0+
- MySQL 5.7+ / PostgreSQL 9.6+ / SQLite 3.8.8+
Installation
Step 1: Install via Composer
composer require bspdx/keystone
Step 2: Publish Configuration & Assets
# Publish the essentials: configuration and migrations php artisan vendor:publish --tag=keystone-config --tag=keystone-migrations # Publish Blade views (optional - only if you want to customize) php artisan vendor:publish --tag=keystone-views # Publish example routes php artisan vendor:publish --tag=keystone-routes # Publish database seeders php artisan vendor:publish --tag=keystone-seeders
Step 3: Run Migrations
php artisan migrate
This will create tables for:
- Two-factor authentication columns in
userstable - Roles and permissions (Spatie)
- Passkeys (Spatie)
- Personal access tokens (Sanctum)
Step 4: Seed Demo Data (Optional)
php artisan db:seed --class=KeystoneSeeder
This creates:
- 4 default roles:
super-admin,admin,editor,user - Common permissions for each role
- 4 demo users (all with password:
password)superadmin@example.com- Super Adminadmin@example.com- Admineditor@example.com- Editoruser@example.com- Regular User
Step 5: Configure Fortify
In your config/fortify.php, ensure these features are enabled:
'features' => [ Features::registration(), Features::resetPasswords(), Features::emailVerification(), Features::updateProfileInformation(), Features::updatePasswords(), Features::twoFactorAuthentication([ 'confirm' => true, 'confirmPassword' => true, ]), ],
Configuration
The package configuration is located at config/keystone.php. Key settings:
Enable/Disable Features
'features' => [ 'registration' => true, 'email_verification' => true, 'two_factor' => true, 'passkeys' => true, 'passkey_2fa' => true, 'api_tokens' => true, ],
RBAC Settings
'rbac' => [ 'multi_tenant' => false, 'default_role' => 'user', 'super_admin_role' => 'super-admin', ],
Passkey Settings
'passkey' => [ 'rp_name' => env('APP_NAME', 'Laravel'), 'rp_id' => env('PASSKEY_RP_ID', 'localhost'), 'user_verification' => 'preferred', 'allow_multiple' => true, 'required_for_roles' => [ // 'admin', ], ],
Two-Factor Settings
'two_factor' => [ 'qr_code_size' => 200, 'recovery_codes_count' => 8, 'required_for_roles' => [ // 'admin', ], ],
Usage
User Model Setup
Add the HasKeystone trait to your User model:
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use BSPDX\Keystone\Traits\HasKeystone; class User extends Authenticatable { use Notifiable, HasKeystone; // ... rest of your model }
This trait combines:
HasApiTokens(Sanctum)TwoFactorAuthenticatable(Fortify)HasRoles(Spatie Permission)HasPasskeys(Spatie Passkeys)
Service Layer (NEW in v0.3.0)
Keystone v0.3.0 introduces a clean service layer architecture to interact with roles, permissions, and passkeys. All external dependencies are now abstracted behind Keystone services.
Using Services in Controllers
<?php namespace App\Http\Controllers; use BSPDX\Keystone\Services\Contracts\RoleServiceInterface; use BSPDX\Keystone\Services\Contracts\PermissionServiceInterface; use BSPDX\Keystone\Services\Contracts\AuthorizationServiceInterface; use BSPDX\Keystone\Services\Contracts\PasskeyServiceInterface; class AdminController extends Controller { public function __construct( private RoleServiceInterface $roleService, private PermissionServiceInterface $permissionService, private AuthorizationServiceInterface $authService ) {} public function assignRole(User $user) { // Get all roles $roles = $this->roleService->getAllWithPermissions(); // Assign roles to user $this->authService->assignRolesToUser($user, ['admin', 'editor']); // Check if user has role if ($this->authService->userHasRole($user, 'admin')) { // User is admin } } }
Benefits:
- Clean dependency injection
- Easy to mock for testing
- No direct external package dependencies in your code
- Future-proof architecture
Blade Components (Optional)
Keystone provides optional pre-built Blade components for Laravel projects. If you're using React, Vue, or another frontend framework, you can skip this section and use the JSON API endpoints instead.
For Laravel Blade users:
Login Form
<x-keystone::login-form :show-passkey-option="true" :show-remember-me="true" :show-register-link="true" :show-forgot-password="true" />
Register Form
<x-keystone::register-form :show-login-link="true" :required-fields="['name', 'email', 'password', 'password_confirmation']" />
Two-Factor Challenge
<x-keystone::two-factor-challenge :show-recovery-code-option="true" />
Passkey Registration
<x-keystone::passkey-register />
Passkey Login
<x-keystone::passkey-login />
Routes
Keystone doesn't auto-register routes. Add them manually from the published examples:
Web Routes (routes/keystone-web.php):
// Include in your routes/web.php require __DIR__.'/keystone-web.php';
API Routes (routes/keystone-api.php):
// Include in your routes/api.php require __DIR__.'/keystone-api.php';
Middleware
Keystone provides three middleware aliases:
Role Middleware
Route::middleware(['auth', 'role:admin'])->group(function () { // Only users with 'admin' role can access }); // Multiple roles (OR logic) Route::middleware(['auth', 'role:admin,editor'])->group(function () { // Users with 'admin' OR 'editor' role can access });
Permission Middleware
Route::middleware(['auth', 'permission:edit-posts'])->group(function () { // Only users with 'edit-posts' permission }); // Multiple permissions Route::middleware(['auth', 'permission:edit-posts,publish-posts'])->group(function () { // Users with either permission can access });
2FA Enforcement Middleware
Route::middleware(['auth', '2fa'])->group(function () { // Ensures users with required roles have 2FA enabled });
Checking Permissions in Code
Traditional Approach (User Model Methods)
// Check role if (auth()->user()->hasRole('admin')) { // User is an admin } // Check permission if (auth()->user()->can('edit-posts')) { // User can edit posts } // Check multiple roles if (auth()->user()->hasAnyRole(['admin', 'editor'])) { // User has at least one of these roles } // Super admin check if (auth()->user()->isSuperAdmin()) { // User is super admin (bypasses all permission checks) }
Service Layer Approach (Recommended for Controllers)
use BSPDX\Keystone\Services\Contracts\AuthorizationServiceInterface; class PostController extends Controller { public function __construct( private AuthorizationServiceInterface $authService ) {} public function edit(Post $post) { if ($this->authService->userHasPermission(auth()->user(), 'edit-posts')) { // User can edit posts } } }
API Usage
Keystone is designed with an API-first architecture, making it perfect for:
- Single Page Applications (React, Vue, Angular, Svelte)
- Mobile applications (iOS, Android, React Native, Flutter)
- Headless/decoupled architectures
- Microservices
Authentication
Use Sanctum for API authentication. All Keystone controllers automatically return JSON when the request has Accept: application/json header or uses wantsJson():
// Login endpoint (you need to create this) Route::post('/login', function (Request $request) { $credentials = $request->validate([ 'email' => 'required|email', 'password' => 'required', ]); if (!Auth::attempt($credentials)) { return response()->json(['message' => 'Invalid credentials'], 401); } $user = $request->user(); $token = $user->createToken('api-token')->plainTextToken; return response()->json([ 'token' => $token, 'user' => $user, ]); });
API Endpoints
All API routes are protected with auth:sanctum middleware. Example requests:
Get All Roles:
curl -X GET http://localhost/api/roles \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Assign Role to User:
curl -X POST http://localhost/api/users/1/roles \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"roles": ["admin"]}'
Enable 2FA:
curl -X POST http://localhost/api/user/two-factor-authentication \
-H "Authorization: Bearer YOUR_TOKEN"
Architecture
Keystone v0.3.0+ uses an API-first, service layer architecture to isolate external dependencies and provide maximum flexibility for any frontend framework.
Service Layer
All role, permission, and passkey operations go through dedicated services:
- PasskeyService - Manages WebAuthn/passkey operations
registerOptions(),register(),authenticationOptions(),authenticate()
- RoleService - Role CRUD and queries
getAllWithPermissions(),create(),delete(),syncPermissions()
- PermissionService - Permission CRUD and queries
getAllWithRoles(),create(),delete(),syncToUser()
- AuthorizationService - High-level authorization operations
assignRolesToUser(),assignPermissionsToUser(),userHasRole(),userHasPermission()
All services are registered in Laravel's service container with interface bindings and convenient aliases:
keystone.passkeykeystone.roleskeystone.permissionskeystone.authorization
Models
Keystone provides its own model classes that extend Spatie's models:
BSPDX\Keystone\Models\KeystoneRole- Extends Spatie's Role model- Adds
isSuperAdmin()method
- Adds
BSPDX\Keystone\Models\KeystonePermission- Extends Spatie's Permission model
All type hints use these Keystone models, providing a consistent BSPDX\Keystone namespace throughout your application.
Benefits
- API-First - Works with any frontend framework (React, Vue, mobile apps, etc.)
- Testability - Mock service interfaces in tests instead of facades
- Maintainability - All external dependencies isolated in service layer
- Flexibility - Easy to swap implementations or add caching/logging
- Clean API - No third-party classes in your controllers
- Optional UI - Blade components included but completely optional
HTTPS Setup
Passkeys require HTTPS! See our detailed guide: HTTPS Setup for Laravel Sail
Quick summary:
-
Install
mkcert:brew install mkcert # macOS mkcert -install -
Generate certificates:
mkdir -p docker/ssl && cd docker/ssl mkcert localhost 127.0.0.1 ::1 mv localhost+2.pem cert.pem mv localhost+2-key.pem key.pem
-
Update
.env:APP_URL=https://localhost SESSION_SECURE_COOKIE=true
-
Configure Nginx/Caddy to use the certificates
See the full guide for detailed instructions.
Multi-Tenancy
Keystone is multi-tenancy ready. To enable:
Step 1: Enable in Configuration
// config/keystone.php 'multi_tenancy' => [ 'enabled' => true, 'tenant_column' => 'tenant_id', 'auto_scope' => true, ], 'rbac' => [ 'multi_tenant' => true, ],
Step 2: Scope Queries
Use Spatie's multitenancy package or implement your own scoping logic.
Testing
Run the package tests:
composer test
Or with PHPUnit directly:
./vendor/bin/phpunit
Customization
Custom Blade Views
Publish the views and modify as needed:
php artisan vendor:publish --tag=keystone-views
Views will be in resources/views/vendor/keystone/.
Custom Styling
All Blade components use CSS custom properties for easy theming:
:root { --keystone-primary: #4f46e5; --keystone-primary-hover: #4338ca; --keystone-danger: #dc2626; --keystone-text: #1f2937; --keystone-border: #d1d5db; --keystone-bg: #ffffff; --keystone-radius: 0.5rem; }
Security
If you discover any security issues, please email info@bspdx.com instead of using the issue tracker.
Credits
- BSPDX
- Built with:
- Laravel Fortify
- Laravel Sanctum
- Spatie Laravel Permission (abstracted)
- Spatie Laravel Passkeys (abstracted)
Note: Starting with v0.3.0, all Spatie dependencies are abstracted through Keystone's service layer, providing a clean BSPDX\Keystone namespace throughout your application.
License
The MIT License (MIT). Please see License File for more information.
Quick Start Example
Here's a complete example to get you started quickly:
1. Install Package
composer require bspdx/keystone php artisan vendor:publish --tag=keystone-config php artisan vendor:publish --tag=keystone-migrations php artisan migrate php artisan db:seed --class=KeystoneSeeder
2. Update User Model
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use BSPDX\Keystone\Traits\HasKeystone; class User extends Authenticatable { use HasKeystone; protected $fillable = ['name', 'email', 'password']; }
3. Create Login Page
<!-- resources/views/auth/login.blade.php --> <!DOCTYPE html> <html> <head> <title>Login</title> <meta name="csrf-token" content="{{ csrf_token() }}"> </head> <body> <x-keystone::login-form /> </body> </html>
4. Add Routes
// routes/web.php Route::get('/login', function () { return view('auth.login'); })->name('login'); // Include Keystone routes require __DIR__.'/keystone-web.php';
5. Test It Out
# Start server (with HTTPS for passkeys) ./vendor/bin/sail up # Visit https://localhost/login # Use demo credentials: admin@example.com / password
That's it! You now have a complete authentication system with 2FA, passkeys, and RBAC.
Support
- Documentation: Full documentation
- Issues: GitHub Issues
- Discussions: GitHub Discussions