birdcar / authkit-laravel
Laravel integration for WorkOS AuthKit
Requires
- php: ^8.3
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- workos/workos-php: ^4.29
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0
- phpstan/phpstan: ^2.0
Suggests
- livewire/livewire: Required for WorkOS Livewire widget components (^3.0)
- dev-main
- v0.7.0
- v0.6.0
- v0.5.0
- v0.4.2
- v0.4.1
- v0.4.0
- v0.3.0
- v0.2.0
- v0.1.0
- dev-release-please--branches--main
- dev-dependabot/composer/pestphp/pest-tw-3.0or-tw-4.0
- dev-dependabot/github_actions/actions/cache-5
- dev-dependabot/github_actions/actions/checkout-6
- dev-dependabot/github_actions/codecov/codecov-action-6
This package is auto-updated.
Last update: 2026-04-11 21:06:52 UTC
README
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.