chuoke / laravel-user-identities
Multi-auth identities for Laravel: decouple auth data from your users table.
Requires
- php: >=8.1
- laravel/framework: ^12|^13
README
A flexible and extensible authentication system for Laravel that enables multiple identity types per user, supporting password-based, OAuth, and token authentication methods.
Core Philosophy
Keep your users table lean - Move authentication data out of users table and into a dedicated identities table. This approach:
- Separates Concerns - User data stays clean, auth data stays organized
- Improves Performance - User queries remain fast, auth queries are optimized
- Enables Flexibility - Add new auth methods without touching users table
- Better Analytics - Clear separation of authentication methods per user
- Enhanced Security - Minimize sensitive data exposure in user queries, logs, and debugging
Features
- 🎯 Separation of Concerns: Decouple authentication data from user business data
- 🔄 Highly Extensible: Easy to add new identity types without schema changes
- ✅ Independent Verification: Track verification status for each identity separately
- 🛡️ Secure by Default: Automatic password hashing and credential protection
- 🔌 Laravel Integration: Custom UserProvider for seamless authentication with default guards
- ⚡ Performance Optimized: Smart caching to minimize database queries
- 🔐 Remember Me: Token stored in identities table, no
remember_tokencolumn needed on users - 🔑 Two-Factor Auth: Fortify 2FA bridge — stores secret & recovery codes as identity, zero user columns
- 🌐 Socialite Ready: One-line OAuth identity binding from Socialite callbacks
- 🔁 Password Rehash: Automatic bcrypt rehash support for Laravel 11+
Installation
composer require chuoke/laravel-user-identities
Publish configuration and migrations:
php artisan vendor:publish --tag=user-identities-config php artisan vendor:publish --tag=user-identities-migrations php artisan migrate
Quick Start
1. Add Traits to User Model
Option A: Extend the package's base User class
use Chuoke\UserIdentities\Auth\User as IdentityUser; class User extends IdentityUser { // All traits are pre-configured with proper method resolution }
Option B: Add traits to your existing User model (recommended)
use Chuoke\UserIdentities\Concerns\HasIdentities; use Chuoke\UserIdentities\Concerns\HasIdentityAuth; class User extends Authenticatable { use HasIdentities, HasIdentityAuth; // HasIdentityAuth overrides getAuthPassword(), getRememberToken(), etc. // to read/write from the identities table instead of user columns. }
2. Configure Laravel Auth
// config/auth.php 'guards' => [ 'web' => [ 'driver' => 'session', // Keep Laravel's default session guard 'provider' => 'user-identity', // Use the identity-aware provider ], ], 'providers' => [ 'user-identity' => [ 'driver' => 'user-identity', 'model' => App\Models\User::class, ], ],
Note: No custom guard driver is needed. Laravel's built-in
SessionGuardworks perfectly with theuser-identityprovider. All identity-specific logic (credential mapping, lookup, validation, remember-me, rehash) is handled entirely in the provider layer.
3. Create Identities
use Chuoke\UserIdentities\Actions\UserIdentityCreate; $action = new UserIdentityCreate(); // Password-based identities $action->execute($user, 'email', 'user@example.com', 'password123'); $action->execute($user, 'mobile', '+1234567890', 'password123'); $action->execute($user, 'username', 'johndoe', 'password123'); // OAuth identities (pre-verified) $action->execute($user, 'github', 'github_id_12345', 'oauth_token_here', true); // Token-based identities $action->execute($user, 'api_key', 'my-app', 'secret_api_key_here', true);
4. Authenticate Users
// Explicit format Auth::attempt([ 'type' => 'email', 'identifier' => 'user@example.com', 'password' => 'password123' ]); // Fortify/Breeze compatible format (via credential_field_mapping) Auth::attempt([ 'email' => 'user@example.com', 'password' => 'password123' ]); // With remember me (token stored in identities table, not users table) Auth::attempt([ 'email' => 'user@example.com', 'password' => 'password123' ], remember: true);
Fortify Two-Factor Authentication
Integrate with Fortify's 2FA without adding any columns to your users table:
use Chuoke\UserIdentities\Concerns\HasIdentities; use Chuoke\UserIdentities\Concerns\HasIdentityAuth; use Chuoke\UserIdentities\Concerns\HasTwoFactorIdentity; use Laravel\Fortify\TwoFactorAuthenticatable; class User extends Authenticatable { use HasIdentities, HasIdentityAuth, HasTwoFactorIdentity, TwoFactorAuthenticatable; }
The HasTwoFactorIdentity trait provides Eloquent attribute accessors that intercept Fortify's access to two_factor_secret, two_factor_recovery_codes, and two_factor_confirmed_at, storing all data as a single identity record:
| Fortify expects (user column) | Stored as (identity) |
|---|---|
two_factor_secret |
type='two_factor' credential JSON .secret |
two_factor_recovery_codes |
type='two_factor' credential JSON .recovery_codes |
two_factor_confirmed_at |
type='two_factor' identity verified_at |
Zero new columns on users table. Zero new columns on identities table.
Socialite Integration
Bind OAuth identities from Socialite callbacks with minimal code:
use Chuoke\UserIdentities\Actions\UserIdentityFindOrCreateFromSocialite; use Chuoke\UserIdentities\Actions\UserIdentitySocialiteBind; use Chuoke\UserIdentities\Actions\AuthenticatableFindByIdentity; // In your Socialite callback controller public function handleCallback(string $provider) { $socialiteUser = Socialite::driver($provider)->user(); // Option 1: Find existing user or create new one $action = new UserIdentityFindOrCreateFromSocialite(); [$user, $identity, $wasCreated] = $action->execute( $provider, $socialiteUser, fn ($su) => User::create(['name' => $su->getName()]) ); Auth::login($user); // Option 2: Bind to current authenticated user $identity = (new UserIdentitySocialiteBind())->execute(Auth::user(), $provider, $socialiteUser); // Option 3: Just look up existing user $user = (new AuthenticatableFindByIdentity())->execute($provider, $socialiteUser->getId()); }
Stored credential data includes token, refresh_token, expires_in, nickname, avatar, email, and name.
Sanctum Compatibility
Sanctum uses its own personal_access_tokens table and does not add columns to the users table. It is fully compatible with this package out of the box — no special configuration needed.
If you want unified identity querying (list all auth methods including Sanctum tokens), you can register a custom SanctumTokenIdentityType that syncs Sanctum tokens as identities.
Migrating Existing Projects
If your project already has email, password, remember_token on the users table, you can migrate to this package without downtime.
Step 1: Enable Fallback Mode
USER_IDENTITIES_FALLBACK=true
With fallback enabled, the package reads from identities when available, but gracefully falls back to user model columns (password, remember_token) when no identity record exists. Your app works exactly as before.
Step 2: Run Data Migration
use Chuoke\UserIdentities\Actions\MigrateUserToIdentities; $migrator = new MigrateUserToIdentities(); // Migrate all users $stats = $migrator->migrateAll( modelClass: \App\Models\User::class, fieldMapping: [ 'email' => 'email', // users.email -> identity type 'email' // 'username' => 'username', // users.username -> identity type 'username' ], migratePassword: true, // Copy password hash (no re-hash needed) migrateRememberToken: true, // Copy remember_token ); // $stats = ['total' => 1000, 'migrated' => 998, 'skipped' => 2]
The migrator copies existing password hashes directly — no re-hashing, no user disruption.
Step 3: Disable Fallback
Once all users are migrated, disable fallback:
USER_IDENTITIES_FALLBACK=false
Step 4 (Optional): Remove Old Columns
Schema::table('users', function (Blueprint $table) { $table->dropColumn(['password', 'remember_token']); // Keep 'email' if you want it for display/search (see Hybrid Mode below) });
Hybrid Mode: Keeping Fields on Users Table
Some fields like email are used very frequently for display, search, and notifications. You may want to keep them on the users table while still using identities for auth.
Setup
// config/user-identities.php 'sync_to_user' => [ 'email' => 'email', // identity type 'email' identifier -> users.email // 'mobile' => 'phone', // identity type 'mobile' identifier -> users.phone ],
// app/Models/User.php use Chuoke\UserIdentities\Concerns\HasIdentities; use Chuoke\UserIdentities\Concerns\HasIdentityAuth; use Chuoke\UserIdentities\Concerns\SyncsIdentityFields; class User extends Authenticatable { use HasIdentities, HasIdentityAuth, SyncsIdentityFields; }
How Sync Works
| Action | Effect |
|---|---|
| Identity email updated | users.email auto-synced |
$user->email = 'new@...' then save() |
Identity identifier auto-synced |
| Manual bulk sync | $user->syncAllIdentitiesToUser() |
| Initial setup from user fields | $user->syncAllUserFieldsToIdentities($password) |
Recommended Architecture
users table:
id, name, email, created_at, updated_at
↑ email kept for display/search/notifications
user_identities table:
type='email', identifier='user@example.com', credentials='$2y$...'
↑ email + password for authentication
↑ auto-synced with users.email
This gives you the best of both worlds:
- ⚡
User::where('email', ...)works fast (no join needed) - 🔒 Auth data is cleanly separated
- 🔄 Data stays consistent automatically
Architecture
Identity Type Hierarchy
IdentityTypeInterface
├── PasswordBasedIdentityType
│ ├── EmailIdentityType (HashCredentialProcessor)
│ ├── MobileIdentityType (HashCredentialProcessor)
│ └── UsernameIdentityType (HashCredentialProcessor)
├── OAuthBasedIdentityType
│ ├── GithubIdentityType (PlainCredentialProcessor)
│ ├── GoogleIdentityType (PlainCredentialProcessor)
│ └── TwitterIdentityType (PlainCredentialProcessor)
└── TokenBasedIdentityType
├── ApiKeyIdentityType (EncryptCredentialProcessor)
├── JwtIdentityType (PlainCredentialProcessor)
└── CustomJwtIdentityType (CustomJwtCredentialProcessor)
Database Schema
The package uses a single user_identities table, keeping your users table clean:
-- users table (lean): -- id, name, created_at, updated_at -- Core user data only -- user_identities table (flexible): -- id, authenticatable_type, authenticatable_id -- type, identifier, credentials, verified_at -- created_at, updated_at
What lives in identities (not users table):
| Capability | Identity type | Stored in |
|---|---|---|
| Email login | email |
identifier + hashed password |
| Mobile login | mobile |
identifier + hashed password |
| Username login | username |
identifier + hashed password |
| OAuth (GitHub, etc.) | github |
provider ID + token JSON |
| API key | api_key |
key name + encrypted secret |
| Remember me token | _remember_token |
system token |
| 2FA secret + codes | two_factor |
JSON credentials |
Core Components
- Traits:
HasIdentities,HasIdentityAuth,HasTwoFactorIdentity,SyncsIdentityFields - Models:
UserIdentity- Main model for identity records - Actions:
UserIdentityCreate,UserIdentityFindOrCreateFromSocialite,UserIdentitySocialiteBind,UserIdentityUpdate,UserIdentityVerify,UserIdentityDelete,UserIdentityPasswordUpdate,MigrateUserToIdentities - Auth:
IdentityUserProvider- Custom user provider with remember-me, rehash, and credential mapping support - Types: Hierarchical identity type classes with built-in behaviors
- Events:
IdentityCreated,IdentityUpdated,IdentityDeleted,IdentityVerified - Processors:
HashCredentialProcessor,EncryptCredentialProcessor,PlainCredentialProcessor
Events
All identity lifecycle operations dispatch events:
| Action | Event |
|---|---|
| Create identity | IdentityCreated |
| Update identity | IdentityUpdated (with $changes array) |
| Delete identity | IdentityDeleted |
| Verify identity | IdentityVerified |
Configuration
// config/user-identities.php return [ // Custom table name 'table' => env('USER_IDENTITIES_TABLE', 'user_identities'), // Register identity types 'types' => [ 'email' => EmailIdentityType::class, // ... ], // Require verification before login 'require_verification' => env('USER_IDENTITIES_REQUIRE_VERIFICATION', true), // Password-syncable identity types 'passwordable_types' => ['email', 'mobile', 'username'], // Map credential field names to identity types (Fortify/Breeze compat) 'credential_field_mapping' => [ 'email' => 'email', 'mobile' => 'mobile', 'username' => 'username', ], // Fallback to user model columns when identity not found (migration support) 'fallback_to_user_model' => env('USER_IDENTITIES_FALLBACK', false), // Sync identity identifiers back to user model columns 'sync_to_user' => [ // 'email' => 'email', ], ];
Requirements
- PHP >= 8.1
- Laravel >= 12.0
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
For detailed API documentation, check the inline code comments in the source files.