Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Passport Multiauth Laravel Package

hamedov/passport-multiauth

Adds multi-guard authentication to Laravel Passport, letting separate user models and providers (admins, customers, etc.) each issue and validate their own OAuth2 tokens through distinct guards. Simplifies per-guard client credentials, token parsing, and route protection in Laravel APIs.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin immediately. With 5 stars and zero dependents this is a "read the source before you ship" dependency. Install explicitly and lock the version:

composer require hamedov/passport-multiauth

Then audit what it actually gives you:

cat vendor/hamedov/passport-multiauth/composer.json   # check the passport constraint
ls vendor/hamedov/passport-multiauth/src              # grant classes, traits, providers

The composer.json constraint against laravel/passport is your first go/no-go signal — Passport's internals changed shape across v9 → v10 → v11 → v12 (League OAuth2 server major bumps, UserRepository signature changes, Passport::grant() vs. AuthorizationServer::enableGrantType()). If the package pins something like ^9.0|^10.0, assume it is dead against your current Passport and budget a fork.

Where to look first:

  1. src/ for the grant class (typically something like Grants/MultiauthGrant.php) and any ServiceProvider that registers it.
  2. config/ if it publishes anything — that's where the provider/guard mapping lives.
  3. README for the exact request contract (what field or header identifies which provider is being authenticated).

First use case: you have users and admins (or customers and staff) as separate Eloquent models with separate providers, and you want a single POST /oauth/token password grant endpoint to authenticate both — something Passport's stock PasswordGrant refuses because it resolves a single UserRepository/provider per client.

Before adopting, check whether native Passport already covers you: since Passport 9, oauth_clients has a provider column, so one client per provider (i.e., one client for admins, one for users) gives you multiauth with zero third-party code. This package is worth it only if you need a single client/endpoint serving multiple providers.

Implementation Patterns

Register the grant(s). The package ships custom grant types; you enable them yourself in AuthServiceProvider::boot():

use Illuminate\Support\ServiceProvider;
use League\OAuth2\Server\AuthorizationServer;
use Laravel\Passport\Passport;

public function boot(): void
{
    $server = app(AuthorizationServer::class);

    $server->enableGrantType(
        app(\Hamedov\PassportMultiauth\Grants\MultiauthGrant::class),
        Passport::tokensExpireIn()
    );
}

Use app(...) rather than new — the grant almost certainly takes the Passport UserRepository and RefreshTokenRepository in its constructor, and resolving from the container keeps you insulated from constructor changes between Passport versions. Verify the exact FQCN in src/ before writing this; the namespace above is a placeholder.

Model side. Add Passport's HasApiTokens to every authenticatable model that can hold a token (User, Admin, Customer). Nothing package-specific is needed there — the package only changes how the token is issued, not how Auth::guard()->user() resolves it.

Request contract. The developer-facing piece is how the caller says which provider to authenticate against. In these packages it is almost always one of:

POST /oauth/token
Content-Type: application/json

{
  "grant_type": "password",
  "client_id": "...",
  "client_secret": "...",
  "username": "[email protected]",
  "password": "...",
  "provider": "admins"
}

or a provider column on the oauth_clients row, or an X-Auth-Provider header. Read the grant's validateRequest()/getUserEntityByUserCredentials() implementation to see which field it reads and whether the value is the guard name or the provider name — they are not interchangeable, and passing the wrong one fails as a generic invalid_grant with no hint.

Guard config for routing after issuance:

'guards' => [
    'api'        => ['driver' => 'passport', 'provider' => 'users'],
    'api-admin'  => ['driver' => 'passport', 'provider' => 'admins'],
],

Then scope routes per guard: Route::middleware('auth:api-admin')->group(...). The grant decides who gets a token; the guard decides who is allowed to use it. Keep those two concerns explicit rather than inferring identity from a role claim.

Integration tip — wrap it in your own endpoint. Don't let clients hit /oauth/token with a free-form provider string. Add a thin controller per audience (/api/admin/token, /api/user/token) that hardcodes the provider and forwards to the grant. That confines the package's contract to one file and gives you a migration path off it later.

Testing:

Passport::actingAs($admin, ['*'], 'api-admin');
$this->getJson('/api/admin/orders')->assertOk();

actingAs bypasses the grant entirely, so it won't catch provider-selection bugs — write at least one integration test that POSTs the real token request for each provider.

Gotchas and Tips

Refresh tokens are the usual hole. A multiauth password grant typically comes with a separate multiauth refresh grant, and it must resolve the user from the same provider that issued the refresh token. If you enable the stock RefreshTokenGrant, a refresh may succeed against the wrong provider or 500 on a null user. Confirm both grants are registered, and store the provider (or an audience claim) on the token so refresh can round-trip it.

invalid_grant tells you nothing. The OAuth2 server deliberately collapses "user not found", "wrong provider", "unhashed password", and "provider misconfigured" into one response. Debug by logging inside the grant, or temporarily instrument the UserRepository — don't guess from the HTTP response.

Provider resolution failures are silent. Laravel returns a null user from an unregistered provider rather than throwing, so a typo in config/auth.php (providers.admins vs provider.admins) presents as bad credentials. php artisan config:clear after every edit; a cached config is a frequent culprit when "it works locally".

Password hashing must match per model. The grant's user repository hashes the incoming password with the app's default hasher. If admins uses a different hash driver, or legacy MD5, authentication fails for that provider only — easy to miss when you tested with users.

passport:install and client setup. You still need the password grant client (php artisan passport:client --password). If the package infers the provider from the client, you need one client per provider; if it takes it from the request, one client is enough. Confirm this before writing your seeding/migration code, because it changes your .env and deployment runbook.

Version pinning and maintenance. With one maintainer and effectively no downstream usage, treat this as vendorable code. Pin the exact version in composer.json, and if the class is small (usually <200 lines), copy it into app/Passport/ and drop the dependency — you'll own the League OAuth2 server upgrades either way, and it removes a supply-chain risk on an auth-critical path.

Extension point if you outgrow it. Everything here is League\OAuth2\Server\Grant\PasswordGrant subclassing. If the package doesn't support what you need (per-provider scopes, MFA step, token audience claims), overriding getUserEntityByUserCredentials() in a 40-line class of your own is more predictable than patching a vendor directory you can't upgrade.

Security review before shipping. Auth packages with no eyes on them deserve a read. Specifically check: is the password compared with Hash::check (not ==), is the provider value validated against an allowlist (an unvalidated provider string can be used to probe which models exist), and does the grant ever fall back to a default provider when the requested one is missing (that's a privilege-escalation shape).

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager