Repository navigation
Releases: roundly-consulting/auth-for-laravel
Releases · roundly-consulting/auth-for-laravel
Release list
1.2.3
Fixed
AccountUserProvider::validateCredentials()returnsfalsefor a password that isn't a string, instead of throwing apassword_verify()TypeError.accounts()->findByKey()returnsnullfor a key that doesn't matchauthentication.key_type(bigint: canonical digits; uuid / ulid: that shape) and sends no query. Before, a malformed key such as1abc,1or1.0could match account 1 on MySQL or SQLite, and PostgreSQL threw a query exception.authentication:logout-everywhereuses the same check, so it refuses a leading-zero or negative key (007,-1); before,007matched account 7.
Security
- The account user provider only looks up accounts by non-empty string credentials (array, numeric and query-closure credentials are refused: they match nobody and send no query) and never returns a disabled account, so a host's Laravel
tokenguard on this provider can't be bypassed with array or numeric input on MySQL (cf. GHSA-f9x3-7vq9-cph6, fixed in laravel/framework 12.68 / 13.29).
1.2.2
Fixed
- Factories build the configured model when a host swaps it.
- Larastan / PHPStan now knows the
$table->authenticationColumns(): voidschema macro that the
published users-table migration and everyauthentication:guardmigration call. A macro only
exists once its provider boots. Larastan boots an auto-discovering host app and usually finds it,
but not where the provider isn't booted during analysis (package development under Testbench,
dont-discover, a provider registered by hand) — there a migration calling it could fail with
"Call to an undefined method". The package ships a PHPStan extension that
phpstan/extension-installerloads automatically (extra.phpstan.includes); without the
installer, includevendor/roundly-consulting/auth-for-laravel/extension.neon.
1.2.1
1.2.0
No breaking changes. Every new argument, event property and DTO field is optional and trailing, so existing calls, new event constructions and listeners keep working unchanged. The mails change only when a host passes actor:, and a host that published the translations gets the new by_actor keys from the package (Laravel merges a published copy over the package's lines).
Added
- Actor-aware notifications.
passwords()->set(),twoFactor()->disable(),passkeys()->remove()andlock()take an optional, trailingactor:(any saved Eloquent model, e.g. the admin). With another actor the owner's mail says an administrator acted (the newnotifications.<type>.by_actor.*copy, en + sk), and thePasswordChanged,TwoFactorDisabled,PasskeyRemovedandAccountLockedevents carry it asactor. The account itself counts as no actor; without one nothing changes. An unsaved actor, or another actor together with a$currenttoken, throwsInvalidArgumentExceptionbefore anything is written. ActorData(the actor's morph type and key, withis()andresolve()),NotificationData::$actorandNotificationType::actorAware(). The queued delivery job carries only this reference, never the actor model.disable(),enable()andunlock()take the same optionalactor:and put it onAccountDisabled,AccountEnabledandAccountUnlocked(actor), so audit listeners read who acted from every account-state event. They still send no notification.
Changed
- Documentation: change a package exception's response with
$exceptions->map()or$exceptions->respond();$exceptions->render()callbacks never see it, because the exception renders itself first. - Maintenance:
roundly-consulting/package-toolkit-for-laravelis now required at^1.3(was^1.0); the facade fix under Security builds on it, socomposer updatepulls it in. The dev-onlytesting-for-laravelfloor is now^1.2.
Security
- Flat facade calls (
Authentication::consumeMagicLink($token),verifyEmailOtp($email, $code),refresh($refreshToken)) no longer leave#[SensitiveParameter]arguments in the facade's stack frame: theAuthenticationfacade uses package-toolkit'sRedactsSensitiveArguments, which hides exactly the arguments theAuthenticationManagermethod marks, while every other argument stays visible. Requires package-toolkit^1.3.
1.1.1
Added
authentication:checkwarns whentwo-factor.attemptsisnullon a guard with two-factor on: two-factor's per-account limiter is then off, the per-challengechallenge.max_attemptsis the only bound on second-factor codes, and every new login opens a fresh budget.
Changed
two_factor.after_email_login = falsenow exempts magic-link, email-code and invitation logins from the second-factor policy only;passkeys.mode = requiredstill makes them enrol a passkey. Before, the setting skipped the mandated enrolment too, sorequiredcould never be enforced for accounts that only sign in by email.- Registering two guards on the same effective route prefix or route-name prefix (from
routes.*, a manualAuthentication::routes()call or->prefix()/->name()) now throwsAuthenticationMisconfigurednaming both guards. Before, Laravel silently kept only the last route per URL androute:cacherefused the duplicate name. Upgrade: give each routed guard its ownroutes.prefixandroutes.name. - Registration and invitation acceptance send the "account exists" notice in the address holder's
preferredLocale(), falling back to the request's / invitation's locale. - Documentation:
EmailChangeRequestedfires only when a confirmation link was issued; it is server-side and must never be shown to the requester.
Fixed
AUTHENTICATION_PASSKEYS=offwith the shipped config no longer fails validation for every guard on the defaults (every endpoint answered 500).passkeys.second_factor = allowedis accepted with passkeys off;requiredandrequired_when_enrolledare still refused.- A lock written while a password login was still hashing now refuses that login, even with the correct password. Failures checked against an account loaded before the lock no longer count, so concurrent failures lock and notify exactly once.
challenge.max_active_per_accountnow holds when logins race: older challenges are superseded again after the new one is inserted.- An email change to a taken address now also sends
EmailChangeRequestedNotificationto the old address (withemail_change.notify_old), so the requester's own mailbox no longer shows whether the address is in use. Its "account exists" notice shares the 10-minute per-address cooldown with registration and invitation acceptance, and goes out in the holder'spreferredLocale(), falling back to the requester's. - Removing a passkey locks the account row while it counts and revokes, so two concurrent removes can no longer delete an account's last way in.
- An explicit re-authentication now satisfies
authentication.reauthenticated:Nroutes whose window is longer thanreauthentication.timeout(the proof is kept for the longer of the timeout and the refresh-token TTL);confirmed_untilis unchanged. authentication:checklists a wrongly typed config value (a non-string class key,notifications.classes.* = false, an invalid*.key_type, another guard's badlaravel_guard) as a problem instead of crashing and hiding the rest.authentication:guard: a re-run no longer adds a secondcreate_<table>_tablemigration (an existing one makes it refuse;--forceoverwrites it in place);--tableis validated like the guard name, and an invalid one writes nothing; the migration, model and factory use the configuredauthentication.columns.password/email_verified_atnames.- Models using
HasAuthenticationalso hide the password column whenauthentication.columns.passwordis renamed. assertTokensInvalidated()under scopealljudges only the sessions active whenactingAsAccount()signed the account in, so a login after the invalidation (passwords.reset.login_after) no longer fails it. Without a guard it resolves the guard likeactingAsAccount(): an account of another guard throws instead of being judged by the default guard's scope, which could pass a missing invalidation.
Security
- A login that raced a password reset, a disable or a logout everywhere no longer leaves a session that keeps refreshing. Sessions record the token version they were issued under; refreshing one the account has moved past revokes it and answers
refresh_invalid. Sessions issued by 1.1.0 or earlier carry no version and are not checked. - A correct password that login refuses with the uniform
invalid_credentials(a disabled or unverified account withoutreveal_account_state, a uniform risk denial, an unavailable step-up) now counts against the login throttle like a wrong one and leaves the lockout counter alone, so the throttle no longer reveals which guess was right. - A passkey second factor or forced passkey enrolment that loses a race to a restarted ceremony now answers
challenge_invalidinstead of skipping the next step; a passkey-plus-TOTP setup could finish without TOTP.
1.1.0
Added
SignInBlockedNotification(notifications.classes.sign_in_blocked, risk reactiondeny) andUnusualSignInNotification(notifications.classes.unusual_sign_in, risk reactionnotify), withNotificationType::SignInBlocked/UnusualSignInand English and Slovak copy. A published config that lacks the new keys still sends them; set one tonullto switch it off.InvitationSendLimitReached(422,invitation_send_limit) for a resend pastinvitations.max_sends.
Changed
- Risk alerts no longer go out as the refresh-token-reuse email (
SuspiciousSessionNotification), whose text said the affected session had been signed out — untrue for a blocked sign-in (no session existed) and for an unusual one (its session is live). A host that swapped or disablednotifications.classes.refresh_token_reuseto change the risk alerts now configuressign_in_blocked/unusual_sign_ininstead. Thenotifications.refresh_token_reuse.reasons.blocked_sign_in/unusual_sign_inlines are gone. - Resending an invitation past
max_sendsthrowsInvitationSendLimitReached(422) instead ofTooManyAttemptswith aRetry-Afterof the whole invitation TTL — the count never resets, so no retry could succeed. Upgrade: catchInvitationSendLimitReached(codeinvitation_send_limit) wherever a resend'sTooManyAttemptswas handled. - Documentation: the README banner uses an absolute image URL, so it also renders on Packagist and other sites.
Fixed
- The
notifications.expiryplural line also covers a count of 0, so it never renders with a leading space. - Concurrent requests could all pass a per-account cooldown (the verification resend and the "account exists" notice) and each send a mail; the cooldown is now one atomic increment.
- Concurrent invitation resends could each mail, ignore the resend cooldown and
max_sends, and under-countsend_count; the count is now incremented in SQL, capped atmax_sends, and the cooldown window is reserved atomically. - Accepting an invitation with a taken address (
invitations.lock_email = false) mailed that address an "account exists" notice every time; it now shares registration's 10-minute per-address cooldown. - Two failed logins reaching
lockout.thresholdtogether both locked the account, dispatchingAccountLockedand mailing the owner twice; a failure could also write a stale counter back over a concurrent one, so the lockout counted too few failures or re-locked after a single failure once a lock expired. attempts_lefton a failed code check (re-authentication) could be too high when another guess ran concurrently, and the exhausted code was not invalidated straight away.- With
activity.store_identifier = hash, the same address typed with a decomposed accent produced a different hash; it is now NFC-composed first, like the throttle keys.
Security
- A locked account's password login now answers exactly like a wrong password (
invalid_credentials, 422) instead oftoo_many_attemptswith the lock'sRetry-After. Previously anyone could tell an existing account from an unknown address by failinglockout.thresholdtimes, and requests against a locked account were never throttled, so one IP could force unlimited password hashing. The attempt now counts against the login throttle like any other guess; the owner still gets theAccountLockedNotification. Upgrade: a client that showed a lock countdown from that 429 now shows its usual wrong-credentials message.
1.0.2
Fixed
- Notification emails now say "1 minute" instead of "1 minutes" in the link and code expiry line, with the correct Slovak plural forms; published
notifications.expiryoverrides without plural forms keep working. - The suspicious-activity email (
SuspiciousSessionNotification) now describes the reason in the recipient's language — Slovak mail no longer contains English reason text such asrefresh_token_reuseorblocked sign-in. The machine-readablereasonvalue in the notification data is unchanged; the wording lives innotifications.refresh_token_reuse.reasons.*.
1.0.1
1.0.0
Initial public release.
Added
- Headless, multi-guard authentication through the
Authenticationfacade: password, magic-link,
email-code and passwordless passkey login. - Multi-step login challenges for TOTP or recovery-code second factors, passkey second factors
and forced two-factor or passkey enrolment. - RS256 access tokens with rotating refresh tokens and device sessions, including logout of one
session, all others or everywhere — built on jwt-for-laravel and refresh-tokens-for-laravel. - Registration and invitations (
register(),invitations()->create()/accept()). - Email verification and verified email change.
- Forgot, reset and change password with a password policy and an optional breached-password check.
- Per-account locale and time zone, plus re-authentication for sensitive actions.
- Login-activity log with throttling, lockout, new-device detection and risk hooks.
- Opt-in JSON endpoints for every flow, with uniform error codes and route middleware
(authentication.verified,authentication.reauthenticated, …). - 18 mail notifications, swappable per guard, and an event for every state change.
- Artisan commands
authentication:install,authentication:guard,authentication:check,
authentication:pruneandauthentication:logout-everywhere. InteractsWithAuthenticationtest helpers (actingAsAccount()with a real token pair,
assertLoginActivity(),assertTokensInvalidated()).- One API in three layers: the
Authenticationfacade, the injectable
AuthenticationManagerand the action classes, all running the same code. Per-guard
sub-contextstwoFactor(),passkeys(),passwords(),email(),invitations(),
reauthentication()andchallenges(), pluslock(),updateLocale(),activity(),
contextFrom()andtokenFrom()on every guard andAuthentication::prune(). Every
per-guard method also works withoutguard()on the default guard
(Authentication::twoFactor()->status($user)). invitations()->create()returns theInvitationLinkit mailed (or, withsend: false,
the link for the host to deliver);invitations()->link()mints a copyable link without
mailing it.reauthentication()->ensureFor(SensitiveAction, …)runs the HTTP layer's sensitive-action
gate for host code acting on behalf of the signed-in user.
Changed
completeChallenge(),invite()andacceptInvitation()moved to
challenges()->complete(),invitations()->create()andinvitations()->accept().- The admin/CLI forms of the credential changes need no caller token:
$currentand
$contextare optional in the two-factor and passkey actions. ResendInvitation,RevokeInvitationandSendInvitationtake the guard name first.- Building-block actions are tagged
@internal; host code issues tokens through
issueTokens()(IssueAccountTokens), which announces them withTokensIssued— also for
actingAsAccount().
Fixed
- Every action taking an account refuses one of another guard's model before it writes —
setting or changing a password, two-factor and passkey changes, disabling, locking,
unlocking and locale updates no longer write first and fail afterwards. - Resending, revoking or re-linking another guard's invitation is refused.
- A manual
lock()notifies the owner exactly like the automatic lockout. email_change.require_reauthentication = falsenow also switches off
ensureFor(SensitiveAction::ChangeEmail), not only the HTTP endpoint.- A login challenge takes its attempt before a TOTP or recovery code is checked, so a parallel
burst can no longer check more codes thanchallenge.max_attemptsallows. - The risk step-up (
require_second_factor) applies to magic-link, email-code, invitation and
registration logins also whentwo_factor.after_email_loginis off; a passkey login that
does not count as MFA must add TOTP. - Registration no longer skips required two-factor enrolment when
two_factor.after_email_loginis off. POST invitationsreturns the link (url), and a link created without mailing
(send: false,link()) no longer counts as a send.- A refresh retires the session's previous access token, so revoking a session kills every
access token it minted. reauthentication.methodsis checked against the factor that actually matched (a recovery
code sent astotp).- A re-authentication no longer makes its device known to new-device detection.
- The users-table stub also adds the passkey user handle, so a fresh install passes
authentication:check. - On/off settings set from
.envas0/off/nowere cast to true
(AUTHENTICATION_LOGIN_PASSWORD=offleft password login on). Every guard switch is now parsed
as a boolean. - A typo in a guard switch (
disabled,maybe) — or inidentifier.normalize/
risk.deny_response— no longer reads as the default: the guard fails to resolve with
AuthenticationMisconfigurednaming the full key, andauthentication:checklists it.