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

0.6.0 2026-01-25 14:38 UTC

This package is auto-updated.

Last update: 2026-01-31 13:47:49 UTC


README

Latest Version on Packagist Total Downloads License

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

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 users table
  • 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 Admin
    • admin@example.com - Admin
    • editor@example.com - Editor
    • user@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.passkey
  • keystone.roles
  • keystone.permissions
  • keystone.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
  • 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:

  1. Install mkcert:

    brew install mkcert  # macOS
    mkcert -install
  2. 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
  3. Update .env:

    APP_URL=https://localhost
    SESSION_SECURE_COOKIE=true
  4. 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

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