Skip to content

Scopes & claims

The provider understands the OIDC standard scopes — openid, profile, email, address, phone — merged with your configured catalog (passport.scopes, below) and any scopes a third party registered directly via Passport::tokensCan(). On a conflict the configured catalog wins, then tokensCan()-registered scopes, then the built-in OIDC scopes — so you can override the description of a standard scope simply by defining it in your catalog.

The package mirrors Passport’s wildcard behavior exactly. Passport treats * as always valid and grants it for the password, personal_access, and client_credentials grants (e.g. $user->createToken('cli', ['*'])). Here * resolves as a scope and survives finalization for those grant types, and is stripped for authorization_code (interactive) flows.

Feed your API scope catalog to the provider through config/oidc.php’s passport.scopes option instead of calling Passport::tokensCan() yourself:

'passport' => [
'token_model' => App\Models\ApiToken::class, // optional Passport token model (a `Laravel\Passport\Token` subclass)
'scopes' => App\Auth\ApiScopes::class, // or an inline [scope => description] map
],

A class-string must implement Bambamboole\LaravelOidc\Server\Contracts\ScopeCatalog (scopes(): array<string, string>). The scope repository consults it lazily — resolved from the container the first time scopes are actually enumerated (the consent screen, the discovery document, token issuance), so a database-backed catalog costs nothing on unrelated requests, keeping key- and db-less artisan runs working, and the result is memoized for the life of the repository. Exceptions thrown by scopes() fall back to an empty catalog; an invalid class-string still fails loudly, at first enumeration rather than at boot.

Scopes registered directly via Passport::tokensCan() (by a third-party package, for instance) are still honored — the repository merges them in, with the configured catalog winning on conflict. Passport::scopes() / scopeIds() no longer reflect oidc.passport.scopes; enumerate the full catalog through the ScopeRepository contract instead.

The scope catalog is provided by the ScopeRepository contract — see Extension contracts to swap it.

Bambamboole\LaravelOidc\Server\Contracts\ClaimsResolver maps an authenticated user to a ClaimSet. A ClaimSet is constructed from a scope => [claim => value] map. Both the id_token builder and the userinfo endpoint call forScopes() on it with the token’s granted scopes, so a claim is only emitted when its scope was granted — and null values are dropped.

use Bambamboole\LaravelOidc\Server\Claims\ClaimSet;
use Bambamboole\LaravelOidc\Server\Contracts\ClaimsResolver;
use Illuminate\Contracts\Auth\Authenticatable;
class AppClaimsResolver implements ClaimsResolver
{
public function resolve(Authenticatable $user): ClaimSet
{
return new ClaimSet([
'profile' => ['name' => $user->name],
'email' => [
'email' => $user->email,
'email_verified' => $user->hasVerifiedEmail(),
],
]);
}
}

Bind your resolver so the provider uses it:

$this->app->singleton(
\Bambamboole\LaravelOidc\Server\Contracts\ClaimsResolver::class,
AppClaimsResolver::class,
);

The bundled DefaultClaimsResolver also maps two conventional user attributes under the profile scope, when present: locale (from $user->locale) and zoneinfo (from $user->timezone). A custom resolver like the one above replaces it entirely, so re-add that mapping yourself if you want to keep it.

The ClaimsResolver and ScopeRepository are the two seams described in full under Extension contracts.