firevel / firebase-authentication
Firebase authentication driver for Laravel
Package info
github.com/firevel/firebase-authentication
pkg:composer/firevel/firebase-authentication
Requires
- php: ^8.2
- illuminate/auth: ^11.0|^12.0|^13.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- kreait/firebase-tokens: ^5.0
- psr/cache: ^3.0
- symfony/cache: ^7.0
Requires (Dev)
- laravel/pint: ^1.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.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
- Configuration Reference
- Usage
- API Reference
- Common Use Cases
- Security Considerations
- Troubleshooting
- Upgrading from v2.x
- Contributing
- License
Features
- JWT Token Verification: Securely verify Firebase Authentication JWT tokens
- Automatic User Sync: Create or update users from Firebase claims, with an opt-out for invite-only flows
- Email Verification Sync: Firebase's
email_verifiedclaim populatesemail_verified_atautomatically - Claim Validation: Type-enforce and sanitize incoming JWT claims (
string,integer,boolean,array,url) — malformed token data is rejected before it ever reaches your database - Lifecycle Events:
FirebaseUserCreated,FirebaseUserUpdated,FirebaseUserResolvedfor plug-in hooks - Anonymous Authentication: Built-in support for Firebase anonymous users
- Microservice Ready: Stateless authentication without database dependency
- Web & API Guards: Session-based exchange endpoint and bearer-token API auth
- Configurable Caching: Use Laravel's Redis/Memcached/database cache for the JWKS cache, not just the local filesystem
- Clock-Skew Tolerance: Optional leeway for token verification
- Laravel Integration: Native integration with Laravel's authentication system
- Flexible User Models: Works with Eloquent, or custom models
Requirements
- PHP 8.2 or higher
- Laravel 11.x, 12.x, 13.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.
Upgrading from v2.x
See UPGRADING.md for the v2 → v3 migration guide.
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\FirebaseAuthenticatable; class User extends Authenticatable { use FirebaseAuthenticatable; protected $fillable = ['name', 'email', 'firebase_id', 'avatar_url']; // Optional: match users by email instead of Firebase UID (default: ['sub' => 'firebase_id']) // protected $firebaseResolveBy = 'email'; // Optional: customize which Firebase claims map to which user attributes // (default: email→email, name→name, avatar_url→picture) // protected $firebaseClaimsMapping = [ // 'email' => 'email', // 'name' => 'name', // ]; }
- Add the
firebase_idandavatar_urlcolumns to your users table:
php artisan vendor:publish --tag=firebase-authentication-migrations php artisan migrate
The published migration is additive: it adds firebase_id (unique, nullable) and avatar_url columns to your existing users table and makes password nullable. The existing id integer primary key is preserved.
- 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 the package config and set project_id there:
php artisan vendor:publish --tag=firebase-authentication-config
// config/firebase-authentication.php return [ 'project_id' => env('FIREBASE_PROJECT_ID', 'your-project-id'), // ... ];
For backwards compatibility, the package also still reads
config('firebase.project_id')andenv('GOOGLE_CLOUD_PROJECT')if the package-namespaced value is unset.
2. Update Authentication Configuration
Modify config/auth.php to use the Firebase driver for API auth:
'guards' => [ 'api' => [ 'driver' => 'firebase', 'provider' => 'users', ], ],
For browser-based auth, see Web Authentication below — the recommended path uses Laravel's
sessiondriver, not thefirebasedriver.
3. Update Your User Model
Add the FirebaseAuthenticatable trait to your User model:
Eloquent Example:
<?php namespace App\Models; use Firevel\FirebaseAuthentication\FirebaseAuthenticatable; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; class User extends Authenticatable { use Notifiable, FirebaseAuthenticatable; /** * The attributes that are mass assignable. * * @var array<string> */ protected $fillable = [ 'name', 'email', 'firebase_id', 'avatar_url', ]; }
4. Create/Update Users Table Migration
Two options, depending on whether you already have a users table.
Option A — existing users table (e.g. Laravel default): publish the bundled migration. It adds firebase_id (unique, nullable), avatar_url, and makes password nullable, leaving the rest of the table intact.
php artisan vendor:publish --tag=firebase-authentication-migrations php artisan migrate
Option B — creating a new users table from scratch:
php artisan make:migration create_users_table
public function up() { Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('firebase_id')->unique()->nullable(); $table->string('name')->nullable(); $table->string('email')->unique()->nullable(); $table->string('avatar_url')->nullable(); $table->string('password')->nullable(); $table->rememberToken(); $table->timestamps(); }); }
php artisan migrate
ℹ️ Account linking & the
subvalues) that share the same email; with the unique index, the second sign-in fails on a duplicate-email error. To support that mode, remove theuniqueconstraint from thefirebase_id, so emails no longer need to be unique).
Microservice Setup (without Database)
For microservices that only need to verify authentication without storing user data, use the FirebaseIdentity model.
FirebaseIdentity stores the Firebase UID on $identity->firebase_id — same shape as the User model — so $request->user()->firebase_id means the same thing across services. The model's id is intentionally unset by default; populate it from a custom claim only if you need integer-id parity with your core service (see Exposing user_id / organization_id from custom claims).
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
3. Exposing user_id / organization_id from custom claims
In most microservice setups you just need to know "who is calling" — $identity->firebase_id is enough, plus $identity->getClaims() for anything else on the token.
When the core service mints Firebase custom claims (via the Admin SDK) carrying its own identifiers, you can expose them as attributes on the identity by subclassing FirebaseIdentity and customizing $firebaseClaimsMapping:
namespace App\Auth; use Firevel\FirebaseAuthentication\FirebaseIdentity; class Identity extends FirebaseIdentity { protected $firebaseClaimsMapping = [ 'id' => 'user_id', // integer id minted by the core service 'organization_id' => 'organization.id', // nested claim via dot notation 'email' => 'email', 'name' => 'name', ]; }
Point the provider at your subclass instead of the package class:
'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Auth\Identity::class, ], ],
Now $request->user()->id is the integer assigned by your core service and $request->user()->organization_id reflects the nested claim. If a claim is missing the attribute is simply not set — there is no silent fallback to the Firebase UID, so misconfigurations stay visible. For this to work the core service must put the matching claims on the token via the Firebase Admin SDK (e.g. auth.setCustomUserClaims($uid, ['user_id' => 42, 'organization' => ['id' => 7]])); without that, the microservice has no way to know those values.
Multiple Guards
You can configure multiple Firebase guards with different providers. This is useful when some routes need database-backed users while others only need token verification (e.g., a registration endpoint for users that don't exist in the database yet).
1. Update Authentication Configuration
In config/auth.php, define two guards with different providers:
'guards' => [ 'api' => [ 'driver' => 'firebase', 'provider' => 'users', // DB-backed User model ], 'register' => [ 'driver' => 'firebase', 'provider' => 'firebase', // FirebaseIdentity (no DB) ], ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], 'firebase' => [ 'driver' => 'eloquent', 'model' => Firevel\FirebaseAuthentication\FirebaseIdentity::class, ], ],
2. Protect Your Routes
Use the appropriate guard for each route:
// Registration endpoint — user may not exist in DB yet Route::middleware('auth:register')->post('/api/register', [RegisterController::class, 'store']); // All other API routes — requires DB-backed user Route::middleware('auth:api')->group(function () { Route::get('/api/profile', [ProfileController::class, 'show']); Route::put('/api/profile', [ProfileController::class, 'update']); });
3. Access User Data in Registration
In your registration controller, you can access the Firebase identity claims to create the database user:
class RegisterController extends Controller { public function store(Request $request) { $identity = $request->user(); // FirebaseIdentity instance $user = User::create([ 'firebase_id' => $identity->firebase_id, 'email' => $identity->email, 'name' => $identity->name, ]); return response()->json($user, 201); } }
Each guard resolves users through its own provider, so the api guard will look up/create users in the database while the register guard returns a lightweight FirebaseIdentity populated from JWT claims.
Web Authentication
There are two ways to authenticate browser users — pick one based on whether your backend needs to hold the raw Firebase token.
| Option A — Laravel session | Option B — Cookie-carried bearer token | |
|---|---|---|
| Backend has the Firebase token to forward | No | Yes |
| Login lifetime | Whatever config('session.lifetime') says |
Whatever the cookie holds (refreshed by client) |
| CSRF & session ergonomics | Standard Laravel | Bypassed (cookie acts as a bearer) |
| Setup | Auto-registered POST /auth/firebase endpoint |
Middleware in the web group |
Option A — Laravel session (recommended)
Exchange a Firebase ID token for a standard Laravel session, then let the session cookie drive web auth like any other Laravel app. The backend never stores the Firebase token; client-side getIdToken() keeps it fresh and re-presents it on demand.
1. Configure a session-driven guard in config/auth.php:
'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'firebase', 'provider' => 'users', ], ],
2. From the browser, POST a fresh Firebase ID token to the auto-registered endpoint:
const idToken = await firebase.auth().currentUser.getIdToken(); await fetch('/auth/firebase', { method: 'POST', credentials: 'include', headers: { Authorization: `Bearer ${idToken}`, 'X-CSRF-TOKEN': csrfToken, // when middleware = 'web' 'X-Requested-With': 'XMLHttpRequest', }, });
After a 200 response, the browser holds a Laravel session cookie and every subsequent web request is authenticated normally. Logout is DELETE /auth/firebase (send the same X-CSRF-TOKEN header).
3. Customize behavior by publishing the config:
php artisan vendor:publish --tag=firebase-authentication-config
That writes config/firebase-authentication.php. The session block controls this flow:
'session' => [ 'enabled' => true, // auto-register the routes 'prefix' => 'auth/firebase', // URL prefix 'middleware' => 'web', // 'web', 'api', or a custom group/array 'guard' => 'web', // which session guard to log into ],
Set firebase-authentication.session.enabled to false if you'd rather wire up your own routes against Firevel\FirebaseAuthentication\Http\Controllers\FirebaseSessionController.
Defaults apply even without publishing — you only need the file if you want to override.
Tradeoff: because the backend doesn't hold the Firebase token, it can't forward it to other Firebase-verifying services as the user. If you need that, send a fresh ID token in the Authorization header on the specific API calls that need it — the Firebase JS SDK guarantees freshness via getIdToken(). This pairs naturally with keeping the api guard above ('driver' => 'firebase') for those calls.
Option B — Legacy: cookie-carried bearer token
Store the raw Firebase ID token in a cookie and have a middleware promote it to an Authorization: Bearer … header on every request. The backend can then read the token any time (useful for forwarding to other services), at the cost of bypassing Laravel's standard session/CSRF flow.
1. Configure the web guard to use the Firebase driver in config/auth.php:
'guards' => [ 'web' => [ 'driver' => 'firebase', 'provider' => 'users', ], ],
2. Add the middleware and exclude the cookie from encryption in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) { $middleware->encryptCookies(except: [ 'bearer_token', // must match firebase-authentication.token_cookie ]); $middleware->web(append: [ \Firevel\FirebaseAuthentication\Http\Middleware\AddAccessTokenFromCookie::class, ]); })
⚠️ The encryption exclusion is required. Laravel's
EncryptCookiesmiddleware runs before this one and silently nulls cookies it cannot decrypt — including a plain Firebase ID token your frontend just set. Without theexcept: [...]entry the middleware appears to do nothing and authentication fails silently. The cookie name must matchconfig('firebase-authentication.token_cookie')(defaultbearer_token).
Your frontend is responsible for keeping the cookie up to date as Firebase ID tokens rotate.
Configuration Reference
After publishing the config with php artisan vendor:publish --tag=firebase-authentication-config, the following options are available in config/firebase-authentication.php. All have sensible defaults — only override what you need.
| Key | Default | Purpose |
|---|---|---|
project_id |
env('GOOGLE_CLOUD_PROJECT') |
Firebase project ID used to verify token issuer/audience. |
token_cookie |
null (falls back to bearer_token) |
Cookie name read by the legacy AddAccessTokenFromCookie middleware. |
leeway |
null |
Seconds of clock skew tolerated when verifying tokens. Set to a small value like 30 if you see sporadic "token used before issued" errors. |
auto_create_users |
true |
Set to false to reject verified tokens whose subject has no matching DB row (invite-only flows). resolveByClaims() returns null instead of creating. |
allow_anonymous |
false |
Whether to accept Firebase anonymous sign-ins. Rejected by default; enable only if your app deliberately supports anonymous users. |
email_verification.enabled |
true |
Sync the email_verified claim to a timestamp column on the user model. |
email_verification.column |
email_verified_at |
Which column receives the timestamp. Only set if currently null — never overwrites an existing value. |
cache.store |
null |
Laravel cache store name (e.g. 'redis'). When set, the package shares the public-key cache via that store. When null, a local FilesystemAdapter is used. |
cache.path |
null |
Custom filesystem cache location when cache.store is null. |
session.enabled |
true |
Auto-register the POST /auth/firebase (login) and DELETE /auth/firebase (logout) routes. |
session.prefix |
auth/firebase |
URL prefix for those routes. |
session.middleware |
web |
Middleware group(s) for the session routes. |
session.guard |
web |
Which session guard the controller logs into. |
Events
The package dispatches three events during token resolution. Wire them up in your EventServiceProvider:
use Firevel\FirebaseAuthentication\Events\FirebaseUserCreated; use Firevel\FirebaseAuthentication\Events\FirebaseUserUpdated; use Firevel\FirebaseAuthentication\Events\FirebaseUserResolved; protected $listen = [ FirebaseUserCreated::class => [SendWelcomeEmail::class, ProvisionTenant::class], FirebaseUserUpdated::class => [LogProfileSync::class], FirebaseUserResolved::class => [LogAuthenticatedRequest::class], ];
Each event exposes $event->user (the resolved model) and $event->claims (the decoded JWT payload).
FirebaseUserResolvedfires on every successful authentication, including unchanged users.FirebaseUserCreatedfires only when a new row is inserted.FirebaseUserUpdatedfires only when an existing row's attributes drift from the token claims.
Using a Shared Cache Backend (Redis/Memcached)
By default the package writes Firebase's signing-key cache to the local filesystem. On ephemeral infrastructure (containers, serverless) this gives every fresh instance a cold cache. Point it at a shared Laravel cache store instead:
// config/firebase-authentication.php 'cache' => [ 'store' => 'redis', // any store name from config/cache.php ],
The store must already be configured in config/cache.php. The package wraps it as a PSR-6 pool via Symfony's Psr16Adapter.
Token Verification Leeway
If your server's clock can drift from Google's signing servers, set a small leeway:
'leeway' => 30, // seconds
Both FirebaseGuard (API requests) and FirebaseSessionController (web exchange endpoint) honor this setting.
Disabling Auto-Creation
For invite-only systems where unknown Firebase users must not become DB users:
'auto_create_users' => false,
When set, resolveByClaims() returns null for tokens whose subject has no matching row. FirebaseGuard treats that as unauthenticated; FirebaseSessionController responds with 401 { "error": "No matching user account." }.
Testing
The package ships a fake token verifier so your application tests don't need real Firebase JWTs. Firevel\FirebaseAuthentication\Testing\FirebaseAuth swaps the contract binding in the container; subsequent requests through FirebaseGuard (or FirebaseSessionController) authenticate against the configured claims regardless of what bearer token is sent.
use Firevel\FirebaseAuthentication\Testing\FirebaseAuth; class ProfileTest extends TestCase { public function test_authenticated_request(): void { FirebaseAuth::actingAs([ 'sub' => 'firebase-uid-1', 'email' => 'tester@example.com', 'email_verified' => true, ]); $this->withHeader('Authorization', 'Bearer anything') ->getJson('/api/profile') ->assertOk(); } public function test_anonymous_user(): void { FirebaseAuth::actingAsAnonymous(); $this->withHeader('Authorization', 'Bearer anything') ->getJson('/api/posts') ->assertForbidden(); } public function test_rejected_token(): void { FirebaseAuth::rejectTokens('Token expired'); $this->withHeader('Authorization', 'Bearer anything') ->getJson('/api/profile') ->assertUnauthorized(); } }
Helpers:
FirebaseAuth::actingAs(array $claims)— verify any token to the given claims.FirebaseAuth::actingAsAnonymous(string $uid = '...')— shortcut for the anonymous claim shape.FirebaseAuth::rejectTokens(string $message = '...')— make verification throw.FirebaseAuth::fake()— bind a fake verifier without configuring claims yet.FirebaseAuth::forget()— unbind the fake (call fromtearDownif your tests share the app).
The fake implements Firevel\FirebaseAuthentication\Contracts\TokenVerifier, the same interface the real KreaitTokenVerifier adapter implements. The full guard + trait + event pipeline runs as in production — only the JWT cryptography is short-circuited.
Email Verification Sync
Add a nullable email_verified_at timestamp column to your users table (Laravel's default users table already has one). Then a sign-in carrying "email_verified": true populates it automatically:
$user = $request->user(); $user->email_verified_at; // Carbon\Carbon $user->hasVerifiedEmail(); // true — when your User implements MustVerifyEmail
The column is set only when currently null, so manual email_verified_at updates from your app are never overwritten by a later token. To disable entirely, set email_verification.enabled to false.
hasVerifiedEmail() and the rest of Laravel's verification API only kick in if your User model implements the Illuminate\Contracts\Auth\MustVerifyEmail interface and uses the matching trait — that's standard Laravel, not something this package adds.
Usage
Basic Authentication
Standard Laravel — protect routes with auth:api, read the user with auth()->user() or $request->user():
Route::middleware('auth:api')->get('/profile', fn (Request $request) => $request->user());
Anonymous Users
Firebase supports anonymous authentication — users that sign in without credentials.
⚠️ Anonymous sign-in is rejected by default. Anonymous tokens carry no email or name, can be issued unbounded, and are usually not what an authenticated route expects. Set
firebase-authentication.allow_anonymoustotrueto accept them.
When enabled, you'll also typically need to:
- Make
emailandnamenullable on youruserstable — anonymous tokens carry neither. - Decide where to gate features per-route using
$user->isAnonymous():
$user = auth()->user(); if ($user->isAnonymous()) { return response()->json(['error' => 'Anonymous users cannot create posts'], 403); } // Full-access user — proceed.
Accessing JWT Claims
The full verified JWT payload is available on the user via getClaims() — useful for reading the sign-in provider, identities, or custom claims set via the Firebase Admin SDK:
$claims = auth()->user()->getClaims(); $provider = $claims['firebase']['sign_in_provider'] ?? null; // 'google.com', 'password', 'anonymous', ... $role = $claims['role'] ?? 'user'; // custom claim, set via Admin SDK
Working with Firebase Tokens
On API requests authenticated through the firebase driver, the raw bearer token is available on the resolved user:
$user = auth()->user(); $token = $user->getFirebaseAuthenticationToken(); // Forward to other Firebase-verifying services, the Admin SDK, etc.
Session-mode caveat: after
POST /auth/firebaseexchanges a token for a Laravel session, the backend no longer holds the Firebase token. On subsequent session-authenticated requests,getFirebaseAuthenticationToken()returnsnull. If you need a fresh token, have the client send it in theAuthorizationheader for the specific call.
Token expiration is enforced automatically — verification fails on expired tokens and auth()->user() returns null.
API Reference
FirebaseAuthenticatable Trait
Methods available on User models using the FirebaseAuthenticatable trait:
The legacy spelling
FirebaseAuthenticable(no secondt) still works in v3 as a deprecated alias — existing models that wroteuse FirebaseAuthenticable;keep working unchanged. New code should preferFirebaseAuthenticatable, which matches Laravel'sAuthenticatablecontract.
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 firebase_id column protected $firebaseResolveBy = ['sub' => 'firebase_id']; // Match by email (when claim name = model attribute) protected $firebaseResolveBy = 'email'; // Match by Firebase UID to a different column protected $firebaseResolveBy = ['sub' => 'firebase_uid'];
Default behavior: ['sub' => 'firebase_id'] — matches Firebase UID (sub claim) to the firebase_id column. The model's own id stays a normal Laravel integer primary key.
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', 'avatar_url' => 'picture', // Firebase's `picture` claim → `avatar_url` column 'phone' => 'phone_number', // Map phone_number claim to phone attribute 'organization_id' => 'organization.id', // Dot notation reads nested claims ];
Default mapping:
email→emailname→nameavatar_url→picture(Firebase'spictureclaim is stored on theavatar_urlattribute)
Nested claims (dot notation): claim keys may use . to drill into nested claim objects — e.g. 'organization_id' => 'organization.id' resolves to $claims['organization']['id']. A literal top-level key always wins over the dotted path if both happen to exist on the token.
$firebaseClaimFilters Property
A validation layer for incoming JWT claims. Each entry runs a claim's raw value through a filter that enforces the expected type, coerces it when safe, and rejects (skips) values that don't validate — so malformed or unexpected token data never reaches your database. Rejected values leave any existing column value untouched (non-destructive).
It's named filter rather than cast deliberately: unlike Eloquent's $casts, a filter can decline to set a value. The reject-or-return semantic mirrors PHP's filter_var — which the integer/boolean filters wrap internally.
protected $firebaseClaimFilters = [ 'picture' => 'url', // keyed by the TOKEN claim, not the model attribute 'age' => 'integer', 'admin' => 'boolean', 'roles' => 'array', // Closure: receives ($value, $claim, $claims); return null to skip. 'name' => fn ($value) => trim($value) ?: null, ];
Keyed by the token claim, not the model attribute. Filtering validates the incoming token data and runs on the raw claim value before it is mapped to an attribute — so the key is the claim name (
picture), not the destination column (avatar_url). Dot-notation claim keys are supported (e.g.'organization.id' => 'integer').
Default (when the property is unset): ['picture' => 'url']. Firebase occasionally sends the picture claim as an oversized inline data: blob URI rather than a hosted URL; the 'url' filter keeps only http/https values and drops the rest, so blobs never get written to avatar_url. Claims with no configured filter keep the default behaviour (scalar → string, anything else skipped).
Built-in filters:
| Name | Accepts | Rejects (→ skipped) |
|---|---|---|
'string' |
any scalar, stored as string | arrays/objects, empty string |
'integer' / 'int' |
int or integer-like string | floats, non-numeric strings, booleans, arrays |
'boolean' / 'bool' |
true/false, 1/0, "true"/"false", "yes"/"no", "on"/"off" |
anything ambiguous |
'array' |
non-empty arrays | scalars, empty arrays |
'url' |
http/https URLs |
data: blobs, other schemes, non-URLs |
A filter may also be a class-string implementing Firevel\FirebaseAuthentication\Contracts\ClaimFilter, a callable (fn ($value, $claim, $claims) => …), or a ClaimFilter instance. An unknown filter name throws InvalidArgumentException.
Custom filter class:
use Firevel\FirebaseAuthentication\Contracts\ClaimFilter; class E164PhoneFilter implements ClaimFilter { public function filter(string $claim, mixed $value, array $claims): mixed { // Return the value to keep it, or null to reject (skip) it. return preg_match('/^\+[1-9]\d{1,14}$/', (string) $value) === 1 ? $value : null; } } // On the model: protected $firebaseClaimFilters = ['phone_number' => E164PhoneFilter::class];
Filters are configured at the model layer only in this version (no config-file key). Set the property on each model that needs custom validation.
transformClaims(array $claims): array
Transforms JWT claims into user attributes using the $firebaseClaimsMapping property, running each claim through its configured $firebaseClaimFilters entry. 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 want to match Firebase users by email rather than by Firebase UID (e.g. you already have a user with that email and want to attach Firebase to it):
// App/Models/User.php class User extends Authenticatable { use FirebaseAuthenticatable; // Match users by email instead of Firebase UID protected $firebaseResolveBy = 'email'; protected $fillable = [ 'name', 'email', 'avatar_url', ]; }
The user model still uses Laravel's default integer id as the primary key. email just becomes the lookup key on each sign-in.
Use case: Migrating from a traditional auth system to Firebase while keeping existing user IDs and matching by email.
⚠️ Heads-up: anonymous Firebase users and phone-only sign-ins have no
firebaseResolveBy = 'email'they resolve tonull(unauthenticated). If you need to support those flows alongside email matching, keepfirebaseResolveBy = ['sub' => 'firebase_id']and write your own lookup-by-email logic where it matters.
Using a Different Firebase UID Column Name
The default v3 column is firebase_id. If you'd rather call it something else (e.g. firebase_uid to match an existing convention), override $firebaseResolveBy:
// App/Models/User.php class User extends Authenticatable { use FirebaseAuthenticatable; // Match Firebase UID (sub claim) to the firebase_uid column protected $firebaseResolveBy = ['sub' => 'firebase_uid']; protected $fillable = [ 'firebase_uid', 'name', 'email', 'avatar_url', ]; }
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('avatar_url')->nullable(); $table->timestamps(); });
Mapping additional claims to columns
For any extra Firebase claim you want stored on the user, add it to $firebaseClaimsMapping and $fillable:
class User extends Authenticatable { use FirebaseAuthenticatable; protected $firebaseClaimsMapping = [ 'email' => 'email', 'name' => 'name', 'avatar_url' => 'picture', 'phone' => 'phone_number', 'locale' => 'locale', ]; protected $fillable = ['name', 'email', 'avatar_url', 'phone', 'locale']; }
To validate or type-coerce those claims (e.g. ensure phone is a string or picture is a real URL), add a $firebaseClaimFilters entry keyed by the claim name. For conditional logic or data transformation beyond that, override transformClaims() — see the API Reference.
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
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)
Users not being created/updated
Problem: User model not syncing with Firebase claims.
Solution:
- Verify
FirebaseAuthenticatabletrait is added to User model - Check
$fillableincludes:['name', 'email', 'firebase_id', 'avatar_url'] - Verify the
firebase_idcolumn exists on the users table (run the bundled migration or add it manually)
Web guard not working
Problem: Authentication works for API but not web routes.
If using Option A (Laravel session):
- Verify
config('auth.guards.web.driver')issession, notfirebase - Confirm
POST /auth/firebasereturns 200 — the client must include the Firebase ID token inAuthorization: Bearer … - If
firebase-authentication.session.middleware = 'web', the request must carry a valid CSRF token; switch to'api'for SPAs that handle CSRF separately - Make sure the browser is keeping cookies across requests (
credentials: 'include'in fetch /withCredentialsin axios)
If using Option B (cookie-carried bearer):
- Ensure
AddAccessTokenFromCookiemiddleware is added to the web middleware group - Most common cause of silent failure: make sure the cookie name is listed in
encryptCookies(except: [...]). Without this, Laravel'sEncryptCookiesmiddleware nulls the cookie before our middleware reads it. - Check that the frontend is setting the cookie (default name:
bearer_token) - Verify cookie domain matches your application domain
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