Outcomes & reasons
Outcomes & reasons
DirectoryAuthenticator::login() and sync() return a final readonly DirectoryOutcome. There are exactly
five statuses; only two are “allow”.
Status taxonomy
status |
ok() |
userId |
reason |
roles |
Meaning |
|---|---|---|---|---|---|
provisioned |
✅ | set | null |
granted | New IAM user created (first login) |
linked |
✅ | set | null |
granted | Existing directory-sourced user reused, roles re-synced |
pending |
❌ | null |
set | [] |
JIT policy blocked it — actionable, retryable |
conflict |
❌ | null |
set | [] |
Email owned by a non-directory account — no takeover |
denied |
❌ | null |
set | [] |
Invalid credentials / connector error |
ok() returns true iff status is provisioned or linked — the only branch in which you should log the
user in.
Reason strings
reason is machine-readable and stable. Treat it as an enum.
status |
reason |
Cause | Where documented |
|---|---|---|---|
pending |
jit_requires_verified_email |
require_verified_email on, email not verified |
JIT & sync |
pending |
jit_domain_not_allowed |
Email domain not in allowed_domains |
Configuration |
pending |
jit_approval_required |
approval_required on |
Configuration |
conflict |
email_taken_non_directory |
Email belongs to a non-directory account | Anti-takeover |
denied |
invalid_credentials |
Connector returned null |
Fail-closed |
Factory methods
DirectoryOutcome::provisioned(string $userId, array $roles): self;
DirectoryOutcome::linked(string $userId, array $roles): self;
DirectoryOutcome::pending(string $reason): self;
DirectoryOutcome::conflict(string $reason): self;
DirectoryOutcome::denied(): self; // reason fixed to 'invalid_credentials'
The constructor is private — an outcome can only be built through these, so the invariants (which fields are
set for which status) always hold.
Branching
Minimal
Full
By reason
if ($outcome->ok()) {
Auth::loginUsingId($outcome->userId);
} else {
back()->withErrors(__('Sign-in failed.'));
}
match ($outcome->status) {
'provisioned', 'linked' => Auth::loginUsingId($outcome->userId),
'pending' => back()->withErrors("Access pending: {$outcome->reason}"),
'conflict' => back()->withErrors('That email belongs to an existing account — manual link required.'),
'denied' => back()->withErrors('Invalid credentials.'),
};
$message = match ($outcome->reason) {
'jit_requires_verified_email' => __('Please verify your email first.'),
'jit_domain_not_allowed' => __('Your email domain is not permitted.'),
'jit_approval_required' => __('Your access is awaiting approval.'),
'email_taken_non_directory' => __('That email is already in use — contact an administrator.'),
'invalid_credentials' => __('Invalid directory credentials.'),
default => null,
};
State diagram
stateDiagram-v2
[*] --> authenticate
authenticate --> denied: connector null
authenticate --> policy: DirectoryUser
policy --> pending: gate fails
policy --> lookup: gate passes
lookup --> provisioned: no existing user
lookup --> linked: existing source=directory
lookup --> conflict: existing non-directory
provisioned --> [*]
linked --> [*]
pending --> [*]
conflict --> [*]
denied --> [*]
pending is retryable; conflict and denied are not (by the user)
A pending clears itself once the blocking condition is resolved (email verified, domain allowed, approval
granted) and the user logs in again. conflict needs an administrator to perform a verified link;
denied needs correct credentials.
Related
- PHP API → DirectoryOutcome — the type itself.
- Directory login — branching in a controller.
- The security pages explain why each non-
okoutcome exists.