designmynight/laravel-mongodb-passport
Laravel MongoDB Passport lets you use Laravel Passport’s OAuth2 server with MongoDB, storing clients, tokens, and related Passport data in Mongo collections instead of a relational database. Ideal for Laravel apps already built on MongoDB.
Prereqs. You need laravel/passport (v10–v12, check the package's composer.json for the exact constraint) and a MongoDB Eloquent driver — either mongodb/laravel-mongodb (v4/v5, the official one) or the legacy jenssegers/mongodb. This package supplies MongoDB-backed replacements for Passport's five models plus a service provider that rebinds them. It does not replace Passport itself; all the OAuth grant logic, JWT signing, and routes are still stock Passport.
composer require designmynight/laravel-mongodb-passport
php artisan vendor:publish --provider="DesignMyNight\\...\\ServiceProvider" # confirm exact namespace from the repo
Steps, in order:
config/database.php (mongodb connection, MONGODB_URI/DB env vars). Verify with php artisan tinker → DB::connection('mongodb')->getDatabaseName().User model a Mongo model — MongoDB\Laravel\Eloquent\Model (or Jenssegers\Mongodb\Eloquent\Model), still implementing Authenticatable and using Laravel\Passport\HasApiTokens.php artisan passport:install. It writes the RSA keys to storage/ and creates the password-grant + personal-access clients. Because the provider has already swapped Passport::clientModel(), those clients land in Mongo collections rather than SQL.php artisan migrate for Passport. Passport's published migrations create MySQL/Postgres tables with a bigint user_id and integer client_id. If you leave them in database/migrations, a fresh migrate will create dead tables and, worse, RefreshDatabase in tests will try to use them. Delete or guard them (see Gotchas).register()):Passport::useClientModel(App\Mongo\Passport\Client::class);
Passport::useTokenModel(App\Mongo\Passport\Token::class);
Passport::useAuthCodeModel(...);
Passport::usePersonalAccessClientModel(...);
Passport::useRefreshTokenModel(...);
First use case to validate the install: issue a personal access token to a Mongo user and hit an authenticated route.
$user = User::create(['name' => 'Ada', 'email' => '[email protected]', 'password' => bcrypt('secret')]);
$token = $user->createToken('cli')->accessToken;
// then: curl -H "Authorization: Bearer $token" http://app.test/api/user
If that round-trips, TokenRepository::find() (JWT jti → Token::find() → Mongo _id lookup) works, and so does Passport::actingAs($user) in feature tests.
Where to read first. The package is small (66 stars). Read the service provider and the five model classes before you build on top of them — the provider tells you exactly which Passport extension points (Passport::use*Model) are used and whether it touches Passport::$clientUuids. That five-minute read answers most of the questions below.
Subclass the package models for anything custom. Don't fork. If you need tenant scoping, extra fields, or soft deletes on tokens:
// app/Mongo/Passport/Token.php
class Token extends \DesignMyNight\...\Token
{
protected $connection = 'mongodb';
protected $collection = 'oauth_access_tokens';
protected $fillable = ['id','user_id','client_id','name','scopes','revoked','expires_at'];
}
Then Passport::useTokenModel(App\Mongo\Passport\Token::class) in a service provider registered after the package's. This is the supported extension point and the reason the use*Model statics exist.
Keep the collection names identical to Passport's SQL table names (oauth_access_tokens, oauth_clients, oauth_auth_codes, oauth_refresh_tokens, oauth_personal_access_clients). Every Passport doc, Stack Overflow answer, and pruning command assumes these names; deviating buys you nothing.
Multi-guard / multi-tenant APIs. Because Mongo _ids are ObjectIds, user_id on a token is an ObjectId, not an int. That means you can keep separate user collections per tenant and still have one Passport install — but the Token::user() morph/relation resolution depends on the token model knowing which model class to instantiate. If you go multi-model, add an explicit user_type field to tokens and override user() rather than relying on Passport's default.
Pruning is your responsibility. Passport's passport:purge works as long as the token model is swapped, but the better Mongo answer is a TTL index so the server reaps expired documents automatically:
Schema::connection('mongodb')->collection('oauth_access_tokens')->createIndex(['expires_at' => 1], ['expireAfterSeconds' => 0]);
Combine: TTL for access tokens (short-lived, high volume), passport:purge --hours=... for refresh tokens and auth codes (which need inspection on reuse detection). Schedule the purge nightly.
Indexes that matter in practice:
| Collection | Index | Why |
|---|---|---|
oauth_access_tokens |
user_id, client_id |
Passport::tokensForUser, client revocation |
oauth_access_tokens |
expires_at (TTL) |
auto-cleanup |
oauth_refresh_tokens |
access_token_id, expires_at (TTL) |
refresh flow, cleanup |
oauth_auth_codes |
expires_at (TTL) |
authorization-code cleanup |
oauth_clients |
personal_access_client, password_client |
passport:client lookups |
Testing pattern. Passport::actingAs($user) avoids the token dance entirely and is the right default for feature tests. Only exercise the real token path in one or two integration tests — and there, use a Mongo test database, not RefreshDatabase against SQL. A DatabaseTransactions trait won't work on a standalone mongod (no replica set), so drop-and-recreate the test DB in setUp instead.
protected function setUp(): void
{
parent::setUp();
DB::connection('mongodb')->getClient()->dropDatabase('app_test');
}
Client creation in CI/seeders. passport:install --no-interaction in a deploy script is fine, but it will silently create duplicate personal-access clients if run repeatedly. Prefer an idempotent seeder that checks PersonalAccessClient::count() first, and pin Passport::personalAccessClientId() in config rather than re-deriving it.
The client_id type is the number-one source of bugs. Passport's JWT aud claim carries the client identifier. With SQL it's an int; with Mongo it's an ObjectId string. Most of the time Client::find($aud) handles this because the Mongo driver coerces a 24-hex string to an ObjectId — but it will not coerce arbitrary strings. If you see "client authentication failed" on a confidential client, dump the JWT payload and compare aud to _id before touching anything else.
Passport::$clientUuids. Passport 12 defaults clients to UUIDs. If the package predates that or sets $clientUuids = false, be explicit in your AuthServiceProvider and stay consistent between existing clients and newly created ones. Mixing UUID clients and ObjectId clients in the same collection will break passport:client output and any Client::find you've hardcoded.
Kill Passport's migrations properly. Deleting the files isn't enough if they're already published and referenced. Either delete them and keep a note in the README, or wrap them:
if (Schema::getConnection()->getDriverName() !== 'mongodb') { ... }
Better: don't publish them at all (Passport::ignoreMigrations() in AuthServiceProvider), which is the cleanest signal to the next developer.
scopes storage shape. SQL Passport stores scopes as a JSON string; the model has no array cast in older versions. Mongo will happily store a native array, and then json_decode on it explodes. If token scopes misbehave, check the package's Token model for an explicit 'scopes' => 'array' cast and a matching TokenRepository override — if the package doesn't handle it, you subclass TokenRepository and rebind it in the container. Test with a scoped token, not just a * token.
expires_at and revoked casts. Passport's TokenRepository compares $token->expires_at->isPast(). If the Mongo model doesn't cast expires_at to datetime, BSON dates come back as MongoDB\BSON\UTCDateTime and that line throws. Same for revoked if it comes back as 1/0. Verify the casts in the package's model — if missing, add them in your subclass immediately.
passport:install writes files, not just rows. The RSA keypair goes to storage/ and is not in Mongo. In containerized deploys, either bake the keys in at build time or mount a persistent volume; otherwise every replica gets different keys and tokens issued by one pod fail on another.
Transactions need a replica set. Passport doesn't wrap grant issuance in a transaction, but if you add your own atomic flows around token creation (e.g. issuing a token and updating a last_login_at field), a single-node mongod will throw "Transaction numbers are only allowed on a replica set member". Develop against a replica set or don't use transactions across Passport's writes.
Debugging checklist, in order:
php artisan route:list — confirm the /oauth/token route exists and Passport's routes are registered.dd(Passport::client()->getKeyType(), Passport::token()->getTable()) (or ->getCollection()) — confirm the swapped models are actually active. Half of all "it doesn't work" reports are a provider not registered or registered before Passport's.Client::first() — if it returns a Client from the SQL driver, your connection isn't set on the model.DB::connection('mongodb')->enableQueryLog()) and inspect the actual filter predicates being sent.Maturity risk is real at 66 stars. The blast radius is five small model classes. If the package has drifted from your Passport version, the cheapest path is often to copy the models into your app (they're ~30 lines each) and keep only the Passport::use*Model() wiring. Budget an hour for that contingency and check the package's composer.json against your Passport constraint before you commit to it.
How can I help you explore Laravel packages today?