authkit-laravel maintained by birdcar
AuthKit Laravel
Laravel integration for WorkOS AuthKit - add enterprise-grade authentication to your Laravel application in minutes.
Features
- AuthKit Authentication - SSO, MFA, social login via WorkOS
- Multi-tenant Organizations - Built-in organization support with role-based access
- Audit Logging - Track user actions with WorkOS Audit Logs
- Webhook Sync - Automatic user/org sync from WorkOS webhooks
- Impersonation - Support user impersonation with visual indicators
- Testing Utilities - Easy testing with
WorkOS::actingAs()
Requirements
- PHP 8.3 or higher
- Laravel 11 or 12
- WorkOS account
Installation
composer require birdcar/authkit-laravel
Run Installation Command
php artisan workos:install
The install command runs an interactive wizard that:
- Detects your environment - Scans for existing auth packages (Breeze, Jetstream, Fortify) and any prior WorkOS setup
- Generates a migration plan - If existing auth is detected, creates a detailed markdown guide at
storage/workos-migration-plan.md - Handles laravel/workos migration - If you're migrating from the official
laravel/workospackage, offers options to replace, augment, or run alongside - Selects components - Lets you choose which parts to install (routes, auth system, webhooks)
- Configures environment - Updates your
.envwith required WorkOS variables - Runs migrations - Optionally runs database migrations
For a minimal install that only publishes config with setup instructions:
php artisan workos:install --mini
Migrating from Existing Auth
If you have Laravel Breeze, Jetstream, or Fortify installed, the wizard detects this and generates a comprehensive migration plan. The plan includes:
- Pre-migration checklist - Backup reminders and WorkOS account setup
- Files to remove - Lists auth controllers and views that are no longer needed
- Database changes - Required schema updates
- User model updates - Traits to add for WorkOS integration
- Data migration options - How to handle existing users (re-authenticate or pre-link via API)
- Post-migration testing - Verification steps
- Rollback plan - How to revert if needed
The migration plan is saved to storage/workos-migration-plan.md for reference.
Migrating from laravel/workos
If you're using the official laravel/workos package, the wizard offers three strategies:
| Strategy | Description |
|---|---|
| Replace | Migrate config and remove the old package (recommended) |
| Augment | Add AuthKit features alongside existing setup |
| Keep both | Install without any migration |
Your existing WORKOS_* environment variables are compatible with AuthKit - no changes needed
Configuration
Environment Variables
Add these to your .env file:
# Required
WORKOS_API_KEY=sk_test_your_api_key
WORKOS_CLIENT_ID=client_your_client_id
WORKOS_REDIRECT_URI=http://localhost:8000/auth/callback
# Set WorkOS as the default auth guard
AUTH_GUARD=workos
# Optional
WORKOS_WEBHOOK_SECRET=your_webhook_secret
Configuration Options
Publish the config file:
php artisan vendor:publish --tag=workos-config
Key options in config/workos.php:
return [
// Your WorkOS credentials
'api_key' => env('WORKOS_API_KEY'),
'client_id' => env('WORKOS_CLIENT_ID'),
'redirect_uri' => env('WORKOS_REDIRECT_URI'),
// Auth guard name
'guard' => 'workos',
// Session configuration
// When true (default), uses WorkOS's wos-session cookie as the source of truth
// When false, stores session data in Laravel's session
'session' => [
'cookie_session' => env('WORKOS_COOKIE_SESSION', true),
'cookie_name' => env('WORKOS_COOKIE_NAME', 'wos-session'),
],
// Your User model
'user_model' => App\Models\User::class,
// Enable/disable features
'features' => [
'organizations' => true,
'impersonation' => true,
],
// Route configuration
'routes' => [
'enabled' => true,
'prefix' => 'auth',
'middleware' => ['web'],
'home' => '/dashboard',
],
// Webhook configuration
'webhooks' => [
'enabled' => true,
'prefix' => 'webhooks/workos',
'sync_enabled' => true,
],
];
Usage
Authentication Routes
The package registers these routes automatically:
| Route | Description |
|---|---|
GET /auth/login |
Redirect to WorkOS AuthKit |
GET /auth/callback |
Handle authentication callback |
GET /auth/logout |
Log out and redirect to WorkOS |
Protecting Routes
Use the workos.auth middleware:
Route::middleware('workos.auth')->group(function () {
Route::get('/dashboard', DashboardController::class);
});
// Or use the auth guard directly
Route::middleware('auth:workos')->group(function () {
// ...
});
Getting the Current User
// Get the authenticated user
$user = auth()->user();
// or
$user = workos()->user();
// Get the current session
$session = workos()->session();
// Check authentication
if (workos()->isAuthenticated()) {
// User is authenticated
}
Organizations
Enable organization support in config:
'features' => [
'organizations' => true,
],
Use the organization middleware to resolve and share the current organization:
// Resolve current organization and share with views
Route::middleware(['workos.auth', 'workos.organization.current'])->group(function () {
// $currentOrganization is available in views
// request()->attributes->get('current_organization') in controllers
});
// Require organization membership (returns 403 if not a member)
Route::middleware(['workos.auth', 'workos.organization'])->group(function () {
// User must belong to the current organization
});
// Require specific role within organization
Route::middleware(['workos.auth', 'workos.organization:admin'])->group(function () {
// User must be an admin of the current organization
});
Working with organizations:
// Organizations are available on the user
$user->organizations; // Collection of organizations
// Get current organization (from WorkOS session)
$currentOrg = $user->currentOrganization();
// Switch organizations (fires OrganizationSwitched event)
$user->switchOrganization('org_456');
// Check membership and roles
$user->belongsToOrganization('org_456'); // bool
$user->hasOrganizationRole('org_456', 'admin'); // bool
Roles and Permissions
Check roles and permissions:
// In PHP
if (workos()->hasRole('admin')) {
// User is admin
}
if (workos()->hasPermission('posts:write')) {
// User can write posts
}
// In Blade
@workosRole('admin')
<p>Admin content</p>
@endworkosRole
@workosPermission('posts:write')
<button>Create Post</button>
@endworkosPermission
Use middleware:
Route::middleware('workos.role:admin')->group(function () {
// Admin-only routes
});
Route::middleware('workos.permission:posts:write')->group(function () {
// Routes requiring write permission
});
Audit Logging
Log user actions to WorkOS Audit Logs:
use WorkOS\AuthKit\Facades\WorkOS;
// Simple audit log
WorkOS::audit('user.updated', [
['type' => 'user', 'id' => '123', 'name' => 'John Doe'],
]);
// With metadata
WorkOS::audit('document.created', [
['type' => 'document', 'id' => 'doc_123', 'name' => 'Q4 Report'],
], [
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
Admin Portal
Generate Admin Portal links:
use WorkOS\AuthKit\Facades\WorkOS;
// Generate SSO configuration link
$link = WorkOS::portal()->generateLink(
organization: $organization->workos_id,
intent: 'sso',
returnUrl: route('settings'),
);
return redirect($link->link);
Available intents:
sso- Configure SSO connectiondsync- Configure Directory Syncaudit_logs- View audit logslog_streams- Configure log streamsdomain_verification- Verify domain ownershipcertificate_renewal- Renew SAML certificates
Webhooks
The package automatically handles these webhook events:
user.created/user.updated- Sync user dataorganization.created/organization.updated- Sync organization dataorganization_membership.created/.updated/.deleted- Sync memberships
Configure your webhook endpoint in WorkOS Dashboard:
https://yourapp.com/webhooks/workos
Impersonation
Detect impersonation in your views:
@impersonating
<div class="alert alert-warning">
You are currently impersonating this user.
</div>
@endimpersonating
Or in PHP:
if (workos()->isImpersonating()) {
// Show impersonation banner
}
Testing
WorkOS::actingAs()
Test authenticated users without hitting WorkOS:
use WorkOS\AuthKit\Facades\WorkOS;
test('authenticated user can view dashboard', function () {
$user = User::factory()->create();
WorkOS::actingAs($user, roles: ['admin'], permissions: ['posts:write']);
$this->get('/dashboard')
->assertOk();
});
test('user with permission can create posts', function () {
$user = User::factory()->create();
WorkOS::actingAs($user, permissions: ['posts:write']);
$this->post('/posts', ['title' => 'Hello'])
->assertCreated();
});
test('user without permission cannot create posts', function () {
$user = User::factory()->create();
WorkOS::actingAs($user, permissions: []);
$this->post('/posts', ['title' => 'Hello'])
->assertForbidden();
});
Faking WorkOS
Replace the WorkOS service with a test fake that captures state and exposes assertions:
use WorkOS\AuthKit\Facades\WorkOS;
test('unauthenticated user is redirected', function () {
$fake = WorkOS::fake();
$this->get('/dashboard')->assertRedirect('/auth/login');
$fake->assertGuest();
})->afterEach(fn () => WorkOS::restore());
test('can build up context incrementally', function () {
$user = User::factory()->create();
$fake = WorkOS::fake()
->actingAs($user, roles: ['member'], permissions: ['todos.read'])
->withRoles(['admin'])
->withPermissions(['todos.write'])
->inOrganization('org_xyz');
$fake->assertHasRole('admin');
$fake->assertHasPermission('todos.write');
$fake->assertInOrganization('org_xyz');
})->afterEach(fn () => WorkOS::restore());
Always pair WorkOS::fake() with ->afterEach(fn () => WorkOS::restore()) or use the InteractsWithWorkOS trait (see below) to prevent fake state from leaking between tests.
InteractsWithWorkOS Trait
The InteractsWithWorkOS trait provides helper methods and handles teardown automatically via Laravel's test lifecycle:
use WorkOS\AuthKit\Testing\Concerns\InteractsWithWorkOS;
describe('todo management', function () {
uses(InteractsWithWorkOS::class);
it('allows authenticated user to view todos', function () {
$user = User::factory()->create();
$fake = $this->actingAsWorkOS($user, roles: ['member'], permissions: ['todos.read']);
$this->get('/dashboard')->assertOk();
$fake->assertHasRole('member');
});
it('activates fake without authentication', function () {
$fake = $this->fakeWorkOS();
$fake->assertGuest();
});
})->afterEach(fn () => WorkOS::restore());
Note: Even with the trait, always include
->afterEach(fn () => WorkOS::restore())when usingdescribe()blocks — Pest'suses()insidedescribe()does not trigger Laravel'ssetUpTraits()auto-teardown.
Audit Assertions
The fake captures all WorkOS::audit() calls so you can assert on them in tests:
test('audit events are captured and assertable', function () {
$user = User::factory()->create();
$fake = WorkOS::fake()->actingAs($user);
// Simulate application code that calls WorkOS::audit()
// In real usage, your controllers/services call WorkOS::audit() — the fake captures those calls
WorkOS::audit('todo.created', metadata: ['title' => 'My Task']);
WorkOS::audit('todo.completed', metadata: ['title' => 'My Task']);
$fake->assertAudited('todo.created');
$fake->assertNotAudited('todo.deleted');
$fake->assertAuditedCount(2);
})->afterEach(fn () => WorkOS::restore());
WorkOSFake API Reference
Setup Methods
| Method | Description |
|---|---|
WorkOS::fake() |
Activate the fake (replaces container binding) |
WorkOS::actingAs($user, ...) |
Activate fake and authenticate user in one call |
WorkOS::restore() |
Tear down fake and restore real service |
WorkOS::isFaked() |
Check if fake is currently active |
$fake->actingAs($user, roles: [], permissions: [], organizationId: null) |
Authenticate a user with optional RBAC context |
$fake->withRoles(['role']) |
Merge additional roles |
$fake->withPermissions(['perm']) |
Merge additional permissions |
$fake->inOrganization('org_id') |
Set organization context |
$fake->impersonating(['email' => '...']) |
Simulate impersonation |
$fake->destroySession() |
Clear authenticated state |
Assertion Methods (all return $fake for chaining)
| Method | Description |
|---|---|
$fake->assertAuthenticated() |
Assert a user is authenticated |
$fake->assertGuest() |
Assert no user is authenticated |
$fake->assertHasRole('role') |
Assert user has a specific role |
$fake->assertHasPermission('perm') |
Assert user has a specific permission |
$fake->assertInOrganization('org_id') |
Assert current organization |
$fake->assertAudited('action', ?callback) |
Assert audit event was logged |
$fake->assertNotAudited('action') |
Assert audit event was NOT logged |
$fake->assertAuditedCount(n) |
Assert total number of audit events |
Inspection Methods
| Method | Description |
|---|---|
$fake->user() |
Get the authenticated user (or null) |
$fake->session() |
Get the current WorkOSSession (or null) |
$fake->isAuthenticated() |
Check if authenticated |
$fake->isImpersonating() |
Check if impersonating |
$fake->organizationId() |
Get current organization ID |
$fake->getAuditedEvents() |
Get all captured audit events |
$fake->clearAuditedEvents() |
Reset captured audit events |
Middleware
| Middleware | Description |
|---|---|
workos.auth |
Require WorkOS authentication |
workos.role:role |
Require specific role |
workos.permission:permission |
Require specific permission |
workos.organization |
Require organization membership |
workos.organization:role |
Require organization role |
workos.organization.current |
Resolve and share current organization |
workos.impersonation |
Detect and expose impersonation state |
workos.inertia |
Share WorkOS data with Inertia.js |
workos.audit |
Log route access to WorkOS Audit Logs |
Blade Directives
| Directive | Description |
|---|---|
@workosRole('role') |
Show content if user has role |
@workosPermission('permission') |
Show content if user has permission |
@impersonating |
Show content when impersonating |
Events
The package dispatches these events:
| Event | When |
|---|---|
UserAuthenticated |
User completes authentication |
UserLoggedOut |
User logs out |
OrganizationSwitched |
User switches organization |
WebhookReceived |
Webhook received from WorkOS |
InvitationSent |
User invitation sent |
InvitationRevoked |
User invitation revoked |
Artisan Commands
| Command | Description |
|---|---|
workos:install |
Interactive wizard to install and configure the package |
workos:install --mini |
Minimal install - config only with setup instructions |
workos:install --force |
Overwrite existing configuration files |
workos:sync-users |
Sync users from WorkOS |
workos:events:listen |
Listen to WorkOS events (development) |
Example Application
The workbench/ directory contains a complete example Todo application. It demonstrates:
- Authentication -- Login/logout via WorkOS AuthKit
- Todo CRUD -- Create, complete, and delete todos scoped per organization
- Organization Switching -- Switch between organizations with separate todo lists
- Role-Based Access Control -- Admin-only delete routes, permission-gated read access
- Audit Logging -- User actions logged via WorkOS Audit Logs API
- Admin Portal -- Links to SSO, Directory Sync, Audit Logs, Log Streams, and Domain Verification
- Testing Patterns -- Pest feature tests using
WorkOS::fake()without real API credentials
Run it locally:
# Clone the repository
git clone https://github.com/birdcar/authkit-laravel.git
cd authkit-laravel
# Install dependencies
composer install
# Start the example app
composer serve
# Reset the database
composer fresh
Contributing
Local Development
# Clone and install
git clone https://github.com/birdcar/authkit-laravel.git
cd authkit-laravel
composer install
# Run tests
composer test
# Run static analysis
composer analyse
# Format code
composer format
# Run example app tests
composer test:example
Submitting Changes
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests (
composer test && composer analyse) - Commit with conventional commit message
- Push and create a Pull Request
Use these PR labels:
major/breaking- Breaking changes (x.0.0)minor/feature/enhancement- New features (0.x.0)patch/fix/bugfix- Bug fixes (0.0.x)skip-release/no-release- Don't create release
License
The MIT License (MIT). See LICENSE for more information.