🔄 Migration Guide¶
This document summarises breaking changes between major versions and how to upgrade. For the full per-release changelog, see CHANGELOG.md.
📋 Index¶
Upgrading to this release — token-version revocation & hashed ephemeral tokens¶
This release ships two additive migrations and several behavioural changes that upgraders must be aware of. There are no destructive schema changes — the schema deltas are the additive users.token_version column and the new auth_webauthn_credentials table.
Required steps¶
Run migrations — two new migrations ship with this release:
Migration
Adds
2026-05-08-000001_add_jwt_token_version_to_usersusers.token_version(int,NOT NULL, default0)2026-06-03-000001_create_webauthn_credentialsauth_webauthn_credentials— the dedicated WebAuthn / passkey credential tablephp spark migrate --all
2026-05-08-000001backs JWT access-token revocation (see below); itsdown()simply drops the column.2026-06-03-000001creates the passkey table; it is only used whenAuthSecurity::$webauthnEnabledistrue, but the table is created regardless so enabling the flag later requires no further migration. See WebAuthn / Passkeys — Storage.Schedule
php spark auth:purge—AuthSecurity::$rememberMePurgeChancenow defaults to0(was20). Expired remember-me tokens are now rejected at validation time regardless, so the old probabilistic on-login purge is pure table maintenance. Move that maintenance to a scheduled command instead:# Run on a schedule (cron / daycry/jobs) php spark auth:purge # purges expired remember-me tokens + terminated device sessions older than 30 days php spark auth:purge --days 7 # use a 7-day retention window for terminated device sessions
auth:purge(command groupAuth) removes expired rows fromauth_remember_tokensand terminatedauth_device_sessionsrows older than--days(default30). If you want to keep inline purging, setAuthSecurity::$rememberMePurgeChanceback to a non-zero value — but a scheduled command is recommended.
Behavioural changes you must know about¶
Change |
What it means for upgraders |
|---|---|
Magic-link / password-reset tokens are now hashed at rest |
|
|
|
Access-token / JWT login logging is fingerprinted |
The login-attempt log ( |
JWT access-token revocation (new token_version)¶
The new users.token_version column powers stateless, denylist-free revocation of JWT access tokens:
JwtControllernow mints the access-token payload as{uid, tv}, wheretvis the user’s currenttoken_version. Legacy scalar payloads (a bare user id) are still accepted — thetvcheck is skipped for them.The
JWTauthenticator’scheck()rejects a token whose embeddedtvdoes not match the user’s currenttoken_version, returninglang('Auth.revokedToken').User::revokeIssuedTokens()bumpstoken_versionatomically, invalidating all outstanding access tokens for that user. It is called automatically byBannable::ban()andServices\PasswordChangeRecorder::record()(on password reset/change). Call it directly for a “log out everywhere” action:$user->revokeIssuedTokens(); // every previously-issued JWT access token now fails check()JwtControllerroutes refresh / logout / issue throughservice('jwtTokenRepository'): refresh is now one-time-use rotation, and logout soft-revokes the refresh token.
Other behaviour now enforced automatically (no action needed)¶
Remember-me expiry & theft detection —
RememberMe::checkRememberMeToken()enforces expiry at validation time (an expired cookie can no longer authenticate). On theft detection (selector matches but validator does not) it purges all of the user’s remember-me tokens, writes thelogin.suspiciousaudit event, and firesEvents::trigger('remember-me-theft', $userId, $selector).TOTP lockout & anti-replay — TOTP / backup-code verification now goes through the same per-user lockout as password login (
UserLockoutManager), and a TOTP code is single-use within its acceptance window (a code at or below the last consumed time-step is rejected).Device-session revocation actually invalidates the live session — when
Auth::$sessionConfig['trackDeviceSessions']istrue, every authenticated request verifies the current PHP session maps to a non-terminatedauth_device_sessionsrow (DeviceSessionModel::isSessionActive()). A remotely-revoked or concurrent-limit-evicted session is forced to re-authenticate. (Previously “revoke” only flipped a DB column and the cookie kept working.)
Optional — opt-in to new behaviour¶
Option |
Default |
Meaning |
|---|---|---|
|
|
Minimum seconds between |
|
|
A Gate ability whose name contains a scope (e.g. |
|
unset ( |
Per provider in |
New explicit OAuth account-linking flow¶
A logged-in user can deliberately link an additional provider via:
Method |
HTTP route |
Route name |
Controller |
|---|---|---|---|
|
|
|
|
The route requires an authenticated user, stashes the current user (session key oauth_link_user_id), and the shared callback links the provider to the current user — no e-mail merge and no verified-email requirement, because the user is authenticated and acting deliberately. Linking a social account already bound to a different local user is refused with lang('Auth.oauthAlreadyLinked').
Filter argument changes¶
Filter |
New argument form |
Effect |
|---|---|---|
|
|
Overrides the global limit/time for that route. |
|
|
Requires a password confirmation no older than |
|
|
Honors the Gate → RBAC fallback ( |
Upgrading to the next release (Unreleased)¶
The Unreleased section in CHANGELOG.md adds 13 features and a handful of internal improvements. No breaking changes — all new behaviour is opt-in.
Required steps¶
Run migrations — six new migrations ship with this release:
Migration
Adds
2026-05-07-000001_add_identities_user_type_revoked_indexComposite index on
auth_identities(user_id, type, revoked_at)2026-05-07-000002_create_audit_logs_tableauth_audit_logs2026-05-07-000003_create_totp_backup_codes_tableauth_totp_backup_codes2026-05-07-000004_add_trusted_until_to_device_sessionsauth_device_sessions.trusted_until2026-05-07-000005_create_password_history_tableauth_password_history2026-05-07-000006_add_password_changed_at_to_usersusers.password_changed_atphp spark migrate --all
Rename test helpers (deprecated, BC kept) — if your tests use the typo’d helpers, the corrected names are now available; the old ones still work as deprecated aliases.
Before (deprecated)
After
$this->inkectMockAttributes(...)$this->injectMockAttributes(...)$this->inkectMockAttributesSecurity(...)$this->injectMockAttributesSecurity(...)$this->inkectMockAttributesOAuth(...)$this->injectMockAttributesOAuth(...)The deprecated names will be removed in v6.
Optional — opt-in to new features¶
Each of these defaults to “off / unchanged” — adopt only what fits your security posture.
Feature |
Where to enable |
Reference |
|---|---|---|
Throttle access-token |
|
|
Concurrent session limit |
|
|
Trusted devices (2FA bypass) |
|
|
TOTP backup codes |
(automatic on TOTP confirmation) |
|
Compromised-password recheck on login |
|
|
Suspicious login alerts |
|
|
Password history (no reuse) |
|
|
Password rotation policy |
|
|
API token scope enforcement |
Apply |
|
Login activity feed |
Wire |
What runs automatically (no action needed)¶
The audit log starts capturing events immediately for: TOTP enable/disable, password changes, lockouts, group/permission grants & revokes, token revocations, JWT logout. Use
auth:auditto read.OauthManager::handleCallback()now compares the OAuthstatewithhash_equals()(timing-safe) — drop-in replacement.UserLockoutManager::recordFailedAttempt()now incrementsfailed_login_countatomically — drop-in replacement.DeviceSessionRecorderno longer propagates DB errors — they are logged and swallowed so a misconfigured tracking table can’t break login.
Upgrading to v5.x¶
What changed¶
OauthManagernow delegates all identity CRUD to a newOAuthTokenRepository.ProfileResolverFactory::create()accepts an optionalarray $providerConfigsecond argument.New OAuth events fire from
OauthManager::handleCallback():oauth-login—(User $user, string $providerName)oauth-profile-fetched—(User $user, string $providerName, array $profileData)
extraJSON on OAuth identities now storesscopes_grantedandprofile_fetched_atalongside the existingrefresh_tokenandprofile.
What you must do¶
If your code… |
Do this |
|---|---|
Calls |
Inject |
Listens for OAuth login via |
Switch to the new |
Uses |
Use |
Relied on the legacy plain-string format in the |
No action required — |
No database migrations are required for the v4 → v5 transition. Existing extra columns continue to work unchanged.
Upgrading to v4.x — Config\Auth split¶
What changed¶
Config\Auth was split into three classes to keep concerns separate. Properties moved according to this table:
Property |
Old class |
New class |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Constructor signatures changed:
PasswordsandBaseValidatoracceptAuthSecurityinstead ofAuth. Custom password validators extendingBaseValidatormust update their type hints.OauthManageracceptsAuthOAuthinstead ofAuth.
What you must do¶
Step 1. Create the two new config files in app/Config/:
// app/Config/AuthSecurity.php
namespace Config;
use Daycry\Auth\Config\AuthSecurity as AuthSecurityConfig;
class AuthSecurity extends AuthSecurityConfig
{
// Move every customised security/lockout/password property here.
public int $minimumPasswordLength = 10;
public int $userMaxAttempts = 5;
// ...
}
// app/Config/AuthOAuth.php
namespace Config;
use Daycry\Auth\Config\AuthOAuth as AuthOAuthConfig;
class AuthOAuth extends AuthOAuthConfig
{
public array $providers = [
// Move your existing $providers array verbatim from app/Config/Auth.php.
];
}
Step 2. Remove the moved properties from app/Config/Auth.php. Anything not in the table above stays in Auth.
Step 3. Search-and-replace setting('Auth.X') → setting('AuthSecurity.X') (or setting('AuthOAuth.X')) for every property listed above. Common offenders:
Before |
After |
|---|---|
|
|
|
|
|
|
|
|
|
|
Step 4. Update custom password validators:
use Daycry\Auth\Authentication\Passwords\BaseValidator;
use Daycry\Auth\Config\AuthSecurity;
class MyValidator extends BaseValidator
{
public function __construct(AuthSecurity $config)
{
parent::__construct($config);
}
}
Step 5. Run the test suite — the type system will catch most missed renames.
General upgrade checklist¶
After any major version bump:
composer update daycry/authphp spark migrate --all— applies any new migrations.composer test— runs PHPUnit + code-style.Review your
app/Config/Auth.php,app/Config/AuthSecurity.php,app/Config/AuthOAuth.phpagainst the published versions for any new options worth adopting (e.g.permissionCacheEnabled,tokenLastUsedThrottle).Check
CHANGELOG.mdfor any non-breaking deprecations to plan ahead of v6.