A production-ready Firebase Authentication driver for Laravel that enables seamless JWT-based authentication using Firebase tokens.
- Features
- Requirements
- Installation
- Quick Start
- Configuration
- Usage
- API Reference
- Common Use Cases
- Security Considerations
- Troubleshooting
- Contributing
- License
- 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
- PHP 8.0 or higher
- Laravel 9.x, 10.x, 11.x, 12.x
- Firebase project with Authentication enabled
Install the package via Composer:
composer require firevel/firebase-authenticationThe package will automatically register its service provider.
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 = 'email'; // use 'email' to automatically create or find user by email
// Optional: Customize which Firebase claims map to which user attributes
protected $firebaseClaimsMapping = [
'email' => 'email',
'name' => 'name',
];
}- 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.
This setup stores user data in your database and syncs it with Firebase claims.
Add your Firebase project ID to .env:
GOOGLE_CLOUD_PROJECT=your-firebase-project-idAlternatively, publish and configure the firebase config:
// config/firebase.php
return [
'project_id' => env('FIREBASE_PROJECT_ID', 'your-project-id'),
];Modify config/auth.php to use the Firebase driver:
'guards' => [
'web' => [
'driver' => 'firebase',
'provider' => 'users',
],
'api' => [
'driver' => 'firebase',
'provider' => 'users',
],
],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',
];
}If using a SQL database, create a migration for the users table:
php artisan make:migration create_users_tablepublic 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 migrateFor microservices that only need to verify authentication without storing user data, use the FirebaseIdentity model.
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\FirebaseIdentityRoute::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
To use Firebase authentication with web routes (session-based), you need to extract the bearer token from cookies.
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,
],
];The middleware reads the raw cookie value directly, so no encryption exclusion configuration is required.
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();
}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}`,
},
});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
}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 authenticationValidate 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);
}Methods available on User models using the FirebaseAuthenticable trait:
Resolves or creates a user from JWT token claims.
$user = (new User)->resolveByClaims($claims);Stores JWT claims on the user instance.
$user->setClaims($claims);Retrieves all JWT token claims.
$claims = $user->getClaims();Checks if the user authenticated anonymously.
if ($user->isAnonymous()) {
// Handle anonymous user
}Stores the raw JWT token.
$user->setFirebaseAuthenticationToken($token);Retrieves the raw JWT token.
$token = $user->getFirebaseAuthenticationToken();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')
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
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;
}The guard is automatically registered and handles authentication. You typically don't interact with it directly, but use Laravel's auth() helper.
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.
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
});// 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']);
});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;
}// 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);
}
}- 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
- 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
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,
];Never commit sensitive credentials. Use .env:
GOOGLE_CLOUD_PROJECT=your-project-id
APP_ENV=production
APP_DEBUG=falseProblem: 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,
],
],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)
Problem: Missing dependencies.
Solution:
composer require kreait/firebase-tokens symfony/cacheProblem: 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
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.
Problem: Authentication works for API but not web routes.
Solution:
- Ensure
AddAccessTokenFromCookiemiddleware is added to web middleware group - Check that frontend is setting the cookie correctly
- Verify cookie domain matches your application domain
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-authenticationContributions are welcome! Please feel free to submit a Pull Request.
- Follow PSR-12 coding standards
- Add tests for new features
- Update documentation for API changes
- Keep backwards compatibility when possible
This package is open-sourced software licensed under the MIT license.
- Issues: GitHub Issues
- Documentation: Firebase Authentication Docs
- Laravel Docs: Authentication