corbosman/laravel-passport-claims
Add custom claims to Laravel Passport access tokens. Drop the provided trait onto your User model, define a getExtraClaims() method, and your extra data is embedded in the JWT for clients to read. Lightweight, no config required.
Laravel Passport mints JWT access tokens with a fixed, minimal claim set (sub, scopes, iss, aud, iat, nbf, exp, jti). Anything else your resource servers need to know about the caller — tenant id, roles, feature flags, display name — normally forces either a database round-trip on every request or a fork of Passport's token internals. This package closes that gap with a single trait method.
Install and wire up:
composer require corbosman/laravel-passport-claims
The service provider is auto-discovered. There is no config file to publish — the extension point is your own model.
Where to look first: open vendor/corbosman/laravel-passport-claims/src and read the HasClaims trait and the service provider. The provider is what swaps the token creation path; the trait is the only thing you touch. Read the README's usage block before anything else — it is short and it is the whole API surface.
First use case — enrich the access token with identity data:
use Corbosman\Passport\HasClaims;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasClaims;
public function getClaims(): array
{
return [
'email' => $this->email,
'name' => $this->name,
'tenant_id' => $this->tenant_id,
'roles' => $this->roles()->pluck('name')->all(),
];
}
}
Issue a token the normal way ($user->createToken('mobile') or a POST /oauth/token with the password grant), paste the access_token into jwt.io, and confirm the claims are present on the payload. If they are, the rest is just discipline about what you put there.
Treat getClaims() as a token-time snapshot, not a live view. It is evaluated once when the access token is minted. Every downstream decision — authorization middleware, tenant scoping, feature gating — reads the value frozen in the JWT until it expires. Design around that: keep tokens short (Passport::tokensExpireIn(now()->addMinutes(15))) so a refresh re-runs getClaims() against current data.
Keep claims small, flat, and stable. The JWT is signed, not encrypted; every claim travels in an HTTP header on every request. Good candidates: tenant_id, org_slug, roles (array of strings), plan, locale, token_version. Bad candidates: nested permission trees, full profile objects, anything over a few hundred bytes.
Multi-tenant scoping is the canonical pattern. Put tenant_id in the token, read it in a middleware, and bind it to a request-scoped container entry or a global Eloquent scope. The request never has to resolve the user to know which tenant it is serving:
class TenantScope
{
public function handle($request, Closure $next)
{
$payload = /* decode $request->bearerToken() */;
abort_unless(isset($payload['tenant_id']), 401);
app()->instance('tenant.id', $payload['tenant_id']);
return $next($request);
}
}
Read claims where you already have the token. Passport's guard resolves the user from sub and does not hand you the parsed payload. Either decode the bearer token in a middleware using the JWT library already in your dependency tree (Lcobucci\JWT ships with modern Passport), or expose the claims you actually need through a small request attribute. Do not re-query the user just to fetch data that is already in the token.
Use a token_version claim for cheap invalidation. Return $this->token_version in getClaims(), bump the column on the user when roles change, and have middleware reject tokens whose version does not match. This gives you near-revocation semantics without a denylist table.
Separate claims from scopes. Scopes (read:orders, write:orders) are Passport's authorization primitive and are checked by $request->user()->tokenCan(...). Claims are identity/context. Don't use a roles claim as your only authorization check unless you accept that the decision is frozen for the token lifetime.
Testing. Passport::actingAs($user, $scopes) fakes the guard and never mints a token, so claims are not exercised. Test getClaims() directly (unit) and mint a real token through the /oauth/token endpoint (feature) when you need to assert the payload. $user->createToken() is the cheapest real-token test.
sub, iss, aud, exp, iat, nbf, jti, and scopes are managed by Passport and the JWT builder. Overriding sub is especially dangerous — Passport uses it to look the user up, so a wrong sub silently breaks authentication or impersonates another user.token_version check.getClaims(). That is usually what you want, but it means a refresh can produce a token with different claims than the one it replaced — expect that in your clients.getClaims() on, so those tokens get the default Passport claim set. Do not assume claims are always present when decoding; always null-coalesce.large_client_header_buffers and Apache's LimitRequestFieldSize cap request-line/header size (often 8 KB by default). A roles array or nested permissions object that grows with the org will eventually return 400s that look like random auth failures. Keep the payload under ~1 KB.getClaims() fail the whole token endpoint. A lazy-loaded relation that throws, an external service call, or a null dereference turns a login into a 500. Keep the method cheap and side-effect free; preload any needed relations and avoid network calls.laravel/passport. Same warning applies if you already customize AccessToken, bind a custom AuthorizationServer, or use another claims package — you are both patching the same seam.auth:api middleware already verified the signature and resolved the user. If you decode separately, do it after auth so you are not trusting an unverified payload.Lcobucci\JWT (or paste into jwt.io). If the token builder throws, you get a LogicException from the JWT library — usually a reserved claim name or an unsupported claim type (objects, Carbon, Eloquent models). Cast everything to scalars or arrays of scalars.getClaims() into a request-aware method.How can I help you explore Laravel packages today?