The post-login pipeline
PostLoginPipeline::register() is a decision hook, not an access-token trigger. Unlike the
access-token triggers on AccessTokenPipeline, it participates
in the interactive login decision itself. Register it in a service provider’s
boot(). It runs exactly once per login attempt — after the primary factor (password) succeeds
and before the MFA challenge is presented — so it is safe to perform side
effects such as audit logging from inside it.
use Bambamboole\LaravelOidc\Server\Authentication\Pipeline\LoginApi;use Bambamboole\LaravelOidc\Server\Authentication\Pipeline\LoginEvent;use Bambamboole\LaravelOidc\Server\Authentication\Pipeline\PostLoginPipeline;
app(PostLoginPipeline::class)->register(function (LoginEvent $event, LoginApi $api): void { if ($event->requestsAcr('mfa')) { $api->requireMfa(); } $api->setIdTokenClaim('tenant', $event->user->tenant_id);});Multiple hooks can be registered; they run in registration order, and the pipeline stops at the first hook that denies the login.
LoginEvent (read side)
Section titled “LoginEvent (read side)”LoginEvent is read-only and describes the attempt:
| Property / method | Description |
|---|---|
$event->user |
The Authenticatable who just passed the primary factor |
$event->client |
The requesting OAuth client (?Bambamboole\LaravelOidc\Server\Clients\Models\Client), or null outside an authorization request |
$event->scopes |
The scopes being requested (list<string>) |
$event->requestedAcrValues |
The acr_values requested by the client (list<string>) |
$event->ip |
The request’s IP address |
$event->userAgent |
The request’s user agent string |
$event->amr |
Authentication methods satisfied so far (list<string>, e.g. ['pwd']) |
$event->authTime |
Unix timestamp of authentication, if already known |
$event->requestsAcr(string $value): bool |
Whether $value is present in requestedAcrValues |
$event->isNewDevice(): bool |
Device-recognition signal — see limitations below |
LoginApi (write side)
Section titled “LoginApi (write side)”LoginApi is used to decide the outcome:
| Method | Effect |
|---|---|
$api->deny(string $reason) |
Denies the login; the user sees a generic authentication failure |
$api->requireMfa() |
Forces the MFA challenge for this login, even if the client didn’t request one |
$api->requireAction(string $key) |
Holds the login until the user completes that required action |
$api->setIdTokenClaim(string $name, mixed $value) |
Queues a claim to be added to the id_token once the login completes |
$api->setAccessTokenClaim(string $name, mixed $value) |
Queues a claim for the interactive access token and its refreshed successors |
setIdTokenClaim() refuses protocol-reserved claim names and logs the attempt instead of applying
it. The reserved set is iss, sub, aud, exp, iat, nbf, jti, nonce, at_hash,
c_hash, auth_time, azp, acr, amr, and sid — so a hook can never forge protocol claims.
setAccessTokenClaim() applies the corresponding access-token protections.
Fail-closed
Section titled “Fail-closed”If a hook throws, the exception is logged and the login is denied — it never falls through to a
permissive default. A denied login discards the recorded factor and returns the same generic
auth.failed message the user would see for bad credentials.
requireMfa() semantics
Section titled “requireMfa() semantics”Calling requireMfa() forces the MFA challenge to be presented. When the user has a challengeable
factor, login defers to the two-factor challenge. When they have none, the
login is held on factor enrollment instead — the same place the realm’s
mfa = always setting sends them. It is denied only when nothing could satisfy the demand: a realm
with no enrollable provider, or one that has switched second factors off with mfa = never.
requireAction() semantics
Section titled “requireAction() semantics”requireAction() holds the login until that action is settled, for this login only — it
expresses a decision about one attempt, not a property of the account. An action that should
outlive the login belongs in a RequiredAction implementation that derives it from state; see
Required actions. A key no action is registered under is dropped and
logged rather than parking the user on a screen that does not exist.
How this emits acr / amr
Section titled “How this emits acr / amr”The queued id_token claims are stored on the session as the login proceeds, and the satisfied
authentication methods accumulate there too — pwd from the password step, plus each verified
factor’s method from the MFA challenge. When the authorization_code grant issues the id_token,
it reads that accumulated set as the amr claim and derives acr from it: 1 for a single
method, 2 when more than one method was satisfied (and no acr at all when none were recorded).
Your queued custom claims are merged onto the same id_token. Because acr/amr are
protocol-reserved, they are owned entirely by the OP — the hook influences them only indirectly, by
calling requireMfa() (which changes how many methods end up satisfied).
Claims queued with setAccessTokenClaim() are stored on the authentication context, applied to the
authorization-code access token, and reissued when that context is continued through refresh.
Current limitations
Section titled “Current limitations”isNewDevice()always returnsfalse(its recognizer treats every device as known) until the device-recognition release ships; there is no real device tracking behind it yet.