Extension contracts
Each of the package’s extension points is a container-bound interface with a default
implementation. Rebind any of them from a service provider’s register() (or boot())
method to replace the behavior without touching a caller.
IssuerResolver
Section titled “IssuerResolver”Bambamboole\LaravelOidc\Server\Shared\Realms\IssuerResolver returns the issuer identifier every
protocol surface builds on: the iss claim of id_tokens, access tokens and logout tokens, the
issuer and endpoint URLs in the discovery document, the audience the oidc guard accepts, and
RFC 9728 resource metadata.
interface IssuerResolver{ public function url(): string;}The default Realms\RealmIssuerResolver returns oidc.issuer, falling back to app.url, with any
trailing slash trimmed, and appends /realms/{realm} when oidc.routes.realms is path. It is
bound as a singleton, so a resolver must derive the issuer from the current request on every call
rather than remember it — under Octane one instance serves many requests.
$this->app->singleton( \Bambamboole\LaravelOidc\Server\Shared\Realms\IssuerResolver::class, PerHostIssuerResolver::class,);Every issuer URL the package emits or validates against goes through this contract, so a rebind changes them consistently. It does not move any route — endpoint paths still come from the routes file, and the discovery document composes them onto the resolved issuer origin.
ScopeRepository
Section titled “ScopeRepository”Bambamboole\LaravelOidc\Server\Scopes\Contracts\ScopeRepository is the catalog of scopes the
provider understands. Every lookup is bound to the resources the request asks for: a scope only
exists under the audiences that own it. An empty $audiences means the realm’s default audience,
its issuer URL — the same convention the token minter follows.
interface ScopeRepository{ /** * @param list<string> $audiences * @return Collection<int, Scope> */ public function all(array $audiences = []): Collection;
/** @param list<string> $audiences */ public function find(string $identifier, array $audiences = []): ?Scope;
/** * The last word on what a token gets: `$requested` is already limited to * known scopes the client is assigned, its default scopes included. * * @param Scope[] $requested * @param list<string> $audiences * @return Scope[] */ public function finalize(array $requested, string $grantType, ?Client $client, ?string $userIdentifier = null, array $audiences = []): array;}The default ConfiguredScopeRepository merges scopes in order: first, the configured
catalog (oidc.scopes) reduced to what the requested resources own; second, the scopes
those resources declare in oidc.resources; third, the built-in OIDC scopes (openid, profile,
email), which hold under every audience. The first occurrence of a scope id wins. Its
finalize() filters out scopes no requested resource owns.
(See Scopes & claims for a deeper look at the merge strategy.) Bind your own to change
the catalog:
$this->app->singleton( \Bambamboole\LaravelOidc\Server\Scopes\Contracts\ScopeRepository::class, MyScopeRepository::class,);ClaimsResolver
Section titled “ClaimsResolver”Bambamboole\LaravelOidc\Server\Scopes\Contracts\ClaimsResolver turns a ClaimsRequest into the
claims to emit.
interface ClaimsResolver{ /** @return array<string, mixed> */ public function resolve(ClaimsRequest $request): array;}The request carries the user, the requesting client (clientId), the granted scopes, and
the audience being built (ClaimsAudience::IdToken or ::Userinfo), so a resolver can vary
claims per client and per surface. ClaimSet remains available for the common scope-gated
case — see Scopes & claims for both shapes.
The default is StandardClaimsResolver. Bind your own:
$this->app->singleton( \Bambamboole\LaravelOidc\Server\Scopes\Contracts\ClaimsResolver::class, AppClaimsResolver::class,);ExchangePolicy
Section titled “ExchangePolicy”Bambamboole\LaravelOidc\Server\Tokens\Contracts\ExchangePolicy authorizes every RFC 8693 token
exchange (and every IssueScopedToken action call).
interface ExchangePolicy{ public function authorize(ExchangeRequest $request): ExchangeGrantResult;}The default AllowlistExchangePolicy enforces audience reciprocity, the target
allowlist, scope narrowing, same-subject, and a lifetime cap — see
Token exchange for the full rules. authorize() must
return an ExchangeGrantResult or throw an
ExchangeDeniedException carrying the RFC error code (invalidGrant(), accessDenied(),
invalidTarget(), invalidScope()); the token endpoint renders it. Rebind it to add tenant checks or
a different allowlist source:
$this->app->singleton( \Bambamboole\LaravelOidc\Server\Tokens\Contracts\ExchangePolicy::class, TenantScopedExchangePolicy::class,);SessionTokenProvider
Section titled “SessionTokenProvider”Bambamboole\LaravelOidc\Server\Shared\Sessions\SessionTokenProvider owns the server-side session
root token used by the browser-fetch flow.
interface SessionTokenProvider{ public function currentToken(): ?string;
public function establish(Authenticatable $user): void;
public function forget(): void;}The default SessionTokenIssuer mints the root token on the Login event,
re-mints it lazily as it nears expiry, and revokes + clears it on the Logout event.
Rebind it to source the root token elsewhere (e.g. an external SSO exchange):
$this->app->singleton( \Bambamboole\LaravelOidc\Server\Shared\Sessions\SessionTokenProvider::class, MyExternalSsoTokenProvider::class,);DeviceRecognizer
Section titled “DeviceRecognizer”Bambamboole\LaravelOidc\Server\Shared\Authentication\DeviceRecognizer decides whether the current
request comes from a device already known for the user — it backs the
LoginEvent::isNewDevice() signal in the post-login pipeline.
interface DeviceRecognizer{ public function isKnown(Authenticatable $user, Request $request): bool;}The default NullDeviceRecognizer returns true for every request (every device is
treated as known), so isNewDevice() is effectively always false until a real
device-recognition release ships. There is no device tracking behind it yet. Bind your
own to add it:
$this->app->singleton( \Bambamboole\LaravelOidc\Server\Shared\Authentication\DeviceRecognizer::class, MyDeviceRecognizer::class,);RequiredAction
Section titled “RequiredAction”Implement it to hold a login until the user has done something your application cares about. See Required actions for the contract, the registry and the screen conventions.
Domain actions
Section titled “Domain actions”Every use case the package’s controllers, commands and jobs perform is an invokable
class under <Domain>\Actions. Controllers validate the request, call the action and
shape the response; the action owns the behavior, including audit events. Actions are
resolved from the container, so binding your own class to an action’s name replaces it
for every caller:
$this->app->bind(RegisterClient::class, App\Oidc\RegisterClientWithApproval::class);| Action | Domain | Called by |
|---|---|---|
RegisterUser, AuthenticateWithPassword, SendPasswordResetLink, ResetPassword, UpdatePassword, ConfirmPassword, SendEmailVerification |
Authentication |
the login, registration, password and verification endpoints |
EnrollFactor, ConfirmFactorEnrollment, RevokeFactor, VerifyFactorChallenge |
Credentials |
the factor enrollment and two-factor challenge endpoints |
LinkSocialAccount, UnlinkSocialAccount |
Brokering |
the social callback and linked-account endpoints |
RegisterClient, ProvisionFirstPartyClient |
Clients |
dynamic client registration, oidc:client, oidc:install-self |
IssueScopedToken |
Tokens |
your application, to mint browser tokens |
EndSession |
Sessions |
the end-session endpoint |
CompleteAuthorization |
Consents |
the consent approve and deny endpoints |
The three contracts Shared\Users\CreateUser, Shared\Users\ResetUserPassword and
Shared\Brokering\CreateUserFromSocialAccount have no default implementation; see
Auth overview.