firevel / firebase-authentication
Firebase authentication driver for Laravel
Installs: 21 924
Dependents: 1
Suggesters: 0
Security: 0
Stars: 17
Watchers: 0
Forks: 8
Open Issues: 1
pkg:composer/firevel/firebase-authentication
Requires
- kreait/firebase-tokens: ^5.0
- symfony/cache: ^7.0
README
A production-ready Firebase Authentication driver for Laravel that enables seamless JWT-based authentication using Firebase tokens.
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Configuration
- Usage
- API Reference
- Common Use Cases
- Security Considerations
- Troubleshooting
- Contributing
- License
Features
- JWT Token Verification: Securely verify Firebase Authentication JWT tokens
- Automatic User Sync: Automatically create/update users from Firebase claims
- Anonymous Authentication: Built-in support for Firebase anonymous users
- Microservice Ready: Stateless authentication without database dependency
- Web & API Guards: Support for both session-based and API authentication
- Token Caching: Optimized token verification with built-in caching
- Laravel Integration: Native integration with Laravel's authentication system
- Flexible User Models: Works with Eloquent, or custom models
Requirements
- PHP 8.0 or higher
- Laravel 9.x, 10.x, 11.x, 12.x
- Firebase project with Authentication enabled
Installation
Install the package via Composer:
composer require firevel/firebase-authentication
The package will automatically register its service provider.
Quick Start
For a quick setup with API authentication:
- Set your Firebase project ID in
.env:
GOOGLE_CLOUD_PROJECT=your-firebase-project-id
- Configure auth guard in
config/auth.php:
'guards' => [ 'api' => [ 'driver' => 'firebase', 'provider' => 'users', ], ],
- Add trait to your User model:
use Firevel\FirebaseAuthentication\FirebaseAuthenticable; class User extends Authenticatable { use FirebaseAuthenticable; public $incrementing = false; protected $fillable = ['name', 'email', 'picture']; // Optional: Customize how users are matched (default: ['sub' => 'id']) protected $firebaseResolveBy = ['sub' => 'id']; // or 'email' to match by email // Optional: Customize which Firebase claims map to which user attributes protected $firebaseClaimsMapping = [ 'email' => 'email', 'name' => 'name', 'picture' => 'picture', ]; }
- Protect your routes:
Route::middleware('auth:api')->get('/user', function (Request $request) { return $request->user(); });
That's it! Send requests with Authorization: Bearer {firebase-jwt-token} header.
Configuration
Standard Setup (with Database)
This setup stores user data in your database and syncs it with Firebase claims.
1. Environment Configuration
Add your Firebase project ID to .env:
GOOGLE_CLOUD_PROJECT=your-firebase-project-id
Alternatively, publish and configure the firebase config:
// config/firebase.php return [ 'project_id' => env('FIREBASE_PROJECT_ID', 'your-project-id'), ];
2. Update Authentication Configuration
Modify config/auth.php to use the Firebase driver:
'guards' => [ 'web' => [ 'driver' => 'firebase', 'provider' => 'users', ], 'api' => [ 'driver' => 'firebase', 'provider' => 'users', ], ],
3. Update Your User Model
Add the FirebaseAuthenticable trait to your User model:
Eloquent Example:
<?php namespace App\Models; use Firevel\FirebaseAuthentication\FirebaseAuthenticable; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; class User extends Authenticatable { use Notifiable, FirebaseAuthenticable; /** * Indicates if the IDs are auto-incrementing. * * @var bool */ public $incrementing = false; /** * The "type" of the primary key ID. * * @var string */ protected $keyType = 'string'; /** * The attributes that are mass assignable. * * @var array<string> */ protected $fillable = [ 'name', 'email', 'picture', ]; }
4. Create/Update Users Table Migration
If using a SQL database, create a migration for the users table:
php artisan make:migration create_users_table
public function up() { Schema::create('users', function (Blueprint $table) { $table->string('id')->primary(); // Firebase UID $table->string('name')->nullable(); $table->string('email')->unique()->nullable(); $table->string('picture')->nullable(); $table->timestamps(); }); }
Run the migration:
php artisan migrate
Microservice Setup (without Database)
For microservices that only need to verify authentication without storing user data, use the FirebaseIdentity model.
1. Update Authentication Configuration
In config/auth.php, configure only the API guard:
'guards' => [ 'api' => [ 'driver' => 'firebase', 'provider' => 'users', ], ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => Firevel\FirebaseAuthentication\FirebaseIdentity::class, ], ],
Laravel 11+ Alternative:
Use the AUTH_MODEL environment variable:
GOOGLE_CLOUD_PROJECT=your-firebase-project-id AUTH_MODEL=Firevel\FirebaseAuthentication\FirebaseIdentity
2. Protect Your Routes
Route::middleware('auth:api')->group(function () { Route::get('/data', [DataController::class, 'index']); Route::post('/process', [ProcessController::class, 'handle']); });
Benefits:
- No database connection required for authentication
- Lightweight and fast
- Perfect for serverless deployments
- User data available from JWT claims
Web Guard Configuration
To use Firebase authentication with web routes (session-based), you need to extract the bearer token from cookies.
1. Add Middleware
In bootstrap/app.php (Laravel 11+):
->withMiddleware(function (Middleware $middleware) { $middleware->web(append: [ \Firevel\FirebaseAuthentication\Http\Middleware\AddAccessTokenFromCookie::class, ]); })
Or in app/Http/Kernel.php (Laravel 10 and below):
protected $middlewareGroups = [ 'web' => [ // ... other middleware \Firevel\FirebaseAuthentication\Http\Middleware\AddAccessTokenFromCookie::class, ], ];
2. Exclude Cookie from Encryption
The bearer_token cookie must not be encrypted.
Laravel 11+ in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) { $middleware->encryptCookies(except: [ 'bearer_token', ]); })
Laravel 10 and below in app/Http/Middleware/EncryptCookies.php:
protected $except = [ 'bearer_token', ];
Usage
Basic Authentication
In Controllers:
use Illuminate\Http\Request; class ProfileController extends Controller { public function show(Request $request) { $user = $request->user(); return response()->json([ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, ]); } }
In Routes:
Route::middleware('auth:api')->get('/profile', function (Request $request) { return $request->user(); });
Manual Authentication Check:
if (auth()->check()) { $userId = auth()->id(); $user = auth()->user(); }
Anonymous Users
Firebase supports anonymous authentication, allowing users to access your app without signing up.
Check if User is Anonymous:
$user = auth()->user(); if ($user->isAnonymous()) { return response()->json([ 'message' => 'Limited features available. Sign up for full access!', 'features' => ['read-only'], ]); } // Regular authenticated user return response()->json([ 'message' => 'Welcome back!', 'features' => ['read', 'write', 'share'], ]);
Conditional Logic Based on Authentication Type:
class PostController extends Controller { public function store(Request $request) { $user = $request->user(); if ($user->isAnonymous()) { return response()->json([ 'error' => 'Anonymous users cannot create posts', ], 403); } // Create post for authenticated user $post = Post::create([ 'user_id' => $user->id, 'title' => $request->title, 'content' => $request->content, ]); return response()->json($post, 201); } }
Frontend Example:
// Sign in anonymously const userCredential = await firebase.auth().signInAnonymously(); const token = await userCredential.user.getIdToken(); // Make API request const response = await fetch('/api/posts', { headers: { 'Authorization': `Bearer ${token}`, }, });
Accessing JWT Claims
All JWT token claims are available through the user model:
$user = auth()->user(); // Get all claims $claims = $user->getClaims(); // Access specific claim data $firebase = $claims['firebase'] ?? []; $signInProvider = $firebase['sign_in_provider'] ?? null; // 'google.com', 'password', 'anonymous', etc. $identities = $firebase['identities'] ?? []; // Check authentication method if ($signInProvider === 'google.com') { // User signed in with Google } elseif ($signInProvider === 'password') { // User signed in with email/password } // Access custom claims (set in Firebase Admin SDK) $customClaims = $claims['custom_claim_name'] ?? null;
Example with Custom Claims:
// Assuming you set custom claims in Firebase: // admin.auth().setCustomUserClaims(uid, { role: 'admin', subscriptionTier: 'premium' }) $user = auth()->user(); $claims = $user->getClaims(); $role = $claims['role'] ?? 'user'; $tier = $claims['subscriptionTier'] ?? 'free'; if ($role === 'admin') { // Grant admin access } if ($tier === 'premium') { // Enable premium features }
Working with Firebase Tokens
Get the Raw JWT Token:
$user = auth()->user(); $token = $user->getFirebaseAuthenticationToken(); // Use token for Firebase Admin SDK operations // or pass to frontend for Firebase Realtime Database/Firestore authentication
Validate Token Expiration:
The package automatically handles token expiration. Expired tokens return null for auth()->user().
$user = auth()->user(); if (!$user) { return response()->json(['error' => 'Unauthorized or token expired'], 401); }
API Reference
FirebaseAuthenticable Trait
Methods available on User models using the FirebaseAuthenticable trait:
resolveByClaims(array $claims): object
Resolves or creates a user from JWT token claims.
$user = (new User)->resolveByClaims($claims);
setClaims(array $claims): self
Stores JWT claims on the user instance.
$user->setClaims($claims);
getClaims(): array
Retrieves all JWT token claims.
$claims = $user->getClaims();
isAnonymous(): bool
Checks if the user authenticated anonymously.
if ($user->isAnonymous()) { // Handle anonymous user }
setFirebaseAuthenticationToken(string $token): self
Stores the raw JWT token.
$user->setFirebaseAuthenticationToken($token);
getFirebaseAuthenticationToken(): ?string
Retrieves the raw JWT token.
$token = $user->getFirebaseAuthenticationToken();
$firebaseResolveBy Property
Controls which attribute is used to match existing users in your database. This determines how the package looks up users when authenticating.
// Default: Match by Firebase UID (sub claim) to id column protected $firebaseResolveBy = ['sub' => 'id']; // Match by email (when claim name = model attribute) protected $firebaseResolveBy = 'email'; // Match by Firebase UID to custom column protected $firebaseResolveBy = ['sub' => 'firebase_uid'];
Default behavior: ['sub' => 'id'] - matches Firebase UID (sub claim) to the id column
Formats:
- Array format
['claim_key' => 'model_attribute']- Use when claim name differs from model attribute (e.g.,['sub' => 'firebase_uid']) - String format
'attribute_name'- Use when claim and model attribute have the same name (e.g.,'email')
$firebaseClaimsMapping Property
Controls how Firebase JWT claims are mapped to user model attributes. Define this property in your User model to customize the mapping:
protected $firebaseClaimsMapping = [ 'email' => 'email', // Model attribute => JWT claim key 'name' => 'name', 'picture' => 'picture', 'phone' => 'phone_number', // Map phone_number claim to phone attribute ];
Default mapping:
email→emailname→namepicture→picture
transformClaims(array $claims): array
Transforms JWT claims into user attributes using the $firebaseClaimsMapping property. Override this method for advanced customization beyond simple mapping:
public function transformClaims(array $claims): array { // Start with the standard mapping $attributes = parent::transformClaims($claims); // Add conditional logic or data transformation if (!empty($claims['email_verified'])) { $attributes['email_verified_at'] = $claims['email_verified'] ? now() : null; } return $attributes; }
FirebaseGuard
The guard is automatically registered and handles authentication. You typically don't interact with it directly, but use Laravel's auth() helper.
Common Use Cases
Matching Users by Email Instead of Firebase UID
If you have an existing user database and want to match Firebase users by email instead of Firebase UID:
// App/Models/User.php class User extends Authenticatable { use FirebaseAuthenticable; // Match users by email instead of Firebase UID protected $firebaseResolveBy = 'email'; // Auto-incrementing integer ID public $incrementing = true; protected $keyType = 'int'; protected $fillable = [ 'name', 'email', 'picture', ]; protected $firebaseClaimsMapping = [ 'email' => 'email', 'name' => 'name', 'picture' => 'picture', ]; }
Migration:
Schema::create('users', function (Blueprint $table) { $table->id(); // Auto-incrementing integer ID $table->string('email')->unique(); $table->string('name')->nullable(); $table->string('picture')->nullable(); $table->timestamps(); });
Use case: When migrating from a traditional authentication system to Firebase, this allows you to keep your existing user IDs and match by email.
Using Custom Firebase UID Column
If you want to store Firebase UID in a separate column while keeping an auto-incrementing primary key:
// App/Models/User.php class User extends Authenticatable { use FirebaseAuthenticable; // Match Firebase UID (sub claim) to firebase_uid column protected $firebaseResolveBy = ['sub' => 'firebase_uid']; public $incrementing = true; protected $keyType = 'int'; protected $fillable = [ 'firebase_uid', 'name', 'email', 'picture', ]; }
Migration:
Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('firebase_uid')->unique(); $table->string('email')->unique()->nullable(); $table->string('name')->nullable(); $table->string('picture')->nullable(); $table->timestamps(); $table->index('firebase_uid'); // Index for faster lookups });
Role-Based Access Control with Custom Claims
// Middleware: app/Http/Middleware/RequireRole.php class RequireRole { public function handle(Request $request, Closure $next, string $role) { $user = $request->user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } $claims = $user->getClaims(); $userRole = $claims['role'] ?? 'user'; if ($userRole !== $role) { return response()->json(['error' => 'Forbidden'], 403); } return $next($request); } } // Route usage Route::middleware(['auth:api', 'role:admin'])->group(function () { Route::get('/admin/users', [AdminController::class, 'users']); });
Syncing Additional User Data
Simple Mapping (Recommended):
Use the $firebaseClaimsMapping property for straightforward claim-to-attribute mapping:
// App/Models/User.php class User extends Authenticatable { use FirebaseAuthenticable; protected $firebaseClaimsMapping = [ 'email' => 'email', 'name' => 'name', 'picture' => 'picture', 'phone' => 'phone_number', // Map phone_number claim to phone 'locale' => 'locale', // Map locale claim to locale ]; protected $fillable = [ 'name', 'email', 'picture', 'phone', 'locale', ]; }
Advanced Mapping with Transformation Logic:
Override transformClaims() when you need conditional logic or data transformation:
// App/Models/User.php public function transformClaims(array $claims): array { // Start with the standard mapping from $firebaseClaimsMapping $attributes = parent::transformClaims($claims); // Add conditional transformations if (!empty($claims['email_verified'])) { $attributes['email_verified_at'] = $claims['email_verified'] ? now() : null; } // Transform data format if (!empty($claims['metadata']['creationTime'])) { $attributes['firebase_created_at'] = Carbon::parse($claims['metadata']['creationTime']); } return $attributes; }
Multi-Tenancy with Firebase
// Set tenant ID as custom claim in Firebase // admin.auth().setCustomUserClaims(uid, { tenantId: 'tenant-123' }) class TenantMiddleware { public function handle(Request $request, Closure $next) { $user = $request->user(); $claims = $user->getClaims(); $tenantId = $claims['tenantId'] ?? null; if (!$tenantId) { return response()->json(['error' => 'No tenant assigned'], 403); } // Set tenant for current request app()->instance('current_tenant', $tenantId); return $next($request); } }
Security Considerations
Token Verification
- All tokens are verified using Firebase's official JWT verification library
- Token signatures are validated against Firebase's public keys
- Token expiration is automatically enforced
- Tokens are cached to improve performance
Best Practices
- Always use HTTPS in production to prevent token interception
- Implement token refresh on the frontend before expiration (tokens expire after 1 hour)
- Never log tokens in production environments
- Use custom claims for roles/permissions instead of storing in database
- Validate user input even for authenticated requests
- Rate limit authentication endpoints to prevent abuse
CORS Configuration
When using API authentication, configure CORS properly:
// config/cors.php return [ 'paths' => ['api/*'], 'allowed_methods' => ['*'], 'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], 'allowed_headers' => ['*'], 'exposed_headers' => [], 'max_age' => 0, 'supports_credentials' => true, ];
Environment Variables
Never commit sensitive credentials. Use .env:
GOOGLE_CLOUD_PROJECT=your-project-id APP_ENV=production APP_DEBUG=false
Troubleshooting
"No users provider found"
Problem: Laravel can't find the users provider.
Solution: Ensure config/auth.php has the provider configured:
'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], ],
"Token verification failed"
Problem: JWT token can't be verified.
Common causes:
- Wrong
GOOGLE_CLOUD_PROJECTenvironment variable - Token expired (tokens are valid for 1 hour)
- Token from wrong Firebase project
- System clock skew
Solution:
- Verify your Firebase project ID matches the token issuer
- Check token expiration on frontend and refresh if needed
- Ensure server time is synchronized (NTP)
"Class 'IdTokenVerifier' not found"
Problem: Missing dependencies.
Solution:
composer require kreait/firebase-tokens symfony/cache
Users not being created/updated
Problem: User model not syncing with Firebase claims.
Solution:
- Verify
FirebaseAuthenticabletrait is added to User model - Check
$fillableincludes:['name', 'email', 'picture'] - Ensure
$incrementing = falseis set - Verify database migration has
idas string column
"No password support for Firebase Users"
Problem: Trying to use password-based authentication methods.
Expected behavior: Firebase JWT authentication doesn't use passwords. This is correct.
Solution: Use Firebase Authentication on the frontend to obtain JWT tokens.
Web guard not working
Problem: Authentication works for API but not web routes.
Solution:
- Ensure
AddAccessTokenFromCookiemiddleware is added to web middleware group - Verify
bearer_tokenis excluded from cookie encryption - Check that frontend is setting the cookie correctly
- Verify cookie domain matches your application domain
Anonymous users can't be identified
Problem: isAnonymous() returns false for anonymous users.
Solution: Ensure you're using the latest version of the package. The anonymous detection was added recently. Update with:
composer update firevel/firebase-authentication
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Guidelines
- Follow PSR-12 coding standards
- Add tests for new features
- Update documentation for API changes
- Keep backwards compatibility when possible
License
This package is open-sourced software licensed under the MIT license.
Support
- Issues: GitHub Issues
- Documentation: Firebase Authentication Docs
- Laravel Docs: Authentication