Hardening pass from the 2026-07-22 security audit. All changes are test-backed.
client_secret is hashed too (OAuthClient::hashSecret() / verifySecret()).
Breaking: existing plaintext values of these ephemeral tokens become invalid
on deploy (no re-hash migration — see MIGRATION note).emailVerified instead of a hard-coded true; auto-linking
to an existing account still requires a verified email. Password reset/change now
revokes all sessions and refresh tokens.ColumnFilterTrait).nonce is stored and echoed in the id_token; and
id_tokens can be signed as RS256 JWTs with a published JWKS (opt-in via a
RsaJwt signer — OidcProvider::getJwks()).allowNoRateLimit: true to opt out).AuthManager::completeTwoFactorLogin() now verifies the TOTP code before
creating a session. Breaking: its signature now requires TotpProvider $totpProvider, string $code.allowInMemoryFallback: true (no more silent no-op). Breaking for new RateLimiter()
with no cache.PdoEmailSender;
refresh-token reuse detection revokes the family; unified strong-password policy on
reset; timing-equalization dummy verify derived from configured Argon2id cost;
opportunistic rehash in token mode; token fragments/PII removed from logs;
Crypto::decrypt(..., strict: true) anti-downgrade mode; SecurityMonitor/ThreatDetector
now persist extra fields in metadata.id (generated with Crypto::randomToken(16), distinct from the token) so it can be
listed and revoked by a non-secret handle. Session::getId()/setId(),
SimpleSession::fromArray()/toArray() (now carry id), and
SessionService::getById()/deleteById() were added.SessionAuthManager::revokeSession($userId, $sessionId) now resolves the session by its
opaque id instead of the token, and resolves via findById() (not validate()) so an
expired session stays revocable. Ownership and missing-session behaviour are unchanged.TotpProvider accepts an optional
RateLimiterInterface; verify()/verifyAndEnable() throttle attempts (keyed on
totp:verify:{userId}) when one is supplied.AuthManager::verifyCredentials() + completeSignIn(). Credential verification and
auth-result creation are now separate single-source primitives, so a caller can gate
(e.g. require 2FA) between verifying the password and creating the session/tokens without
verifying the password twice.PdoTotpStorage now persists the TOTP algorithm, so the SHA-1 → SHA-256 migration state
survives storage round-trips (it was previously always read back as SHA-1).SessionRepositoryInterface gains two methods: findById(string $id): ?Session and
deleteById(string $id): bool. Any custom implementation must add them.PdoSessionRepository::createTable() now creates an id VARCHAR(64) NOT NULL column with a
UNIQUE constraint. Since the DDL is CREATE TABLE IF NOT EXISTS, existing PDO
deployments are not migrated automatically — add the column manually, e.g.
ALTER TABLE sessions ADD COLUMN id VARCHAR(64) NULL, ADD UNIQUE KEY uniq_id (id);
then backfill ids for current rows. (Doctrine/Symfony migration is handled separately.)SessionAuthManager::signUp() writes the hashed password under the canonical
password_hash key (was password), so user creation through PdoUserRepository no
longer persists a NULL password.requires2fa: removed a dead 24-hour branch (DateInterval::$h is always < 24) so
the elapsed-time check is correct.refresh_token;
it returns token_type: session instead.INTEGER PRIMARY KEY AUTOINCREMENT with INT AUTO_INCREMENT PRIMARY KEY in
PdoTotpStorage and PdoEmailVerificationStorage, and the SQLite/PostgreSQL
ON CONFLICT … DO UPDATE upsert in PdoTotpStorage::store() with MySQL
ON DUPLICATE KEY UPDATE. Previously the DDL mixed dialects and ran cleanly on no single
engine. For SQLite/PostgreSQL, supply your own storage implementation.PdoTotpStorage::useBackupCode()
compared the submitted code against stored hashes with in_array(), which always
failed — backup codes (and 2FA recovery/disable) were effectively unusable. They are
now verified with password_verify().PdoTotpStorage accepts an optional master key
(3rd constructor argument) and encrypts the TOTP seed with AES-256-GCM. A DB read no
longer exposes usable TOTP secrets.PdoSessionRepository and
PdoRefreshTokenRepository store sha256(token) (and replaced_by), look up by hash,
and re-inject the plaintext token on the returned entity. A DB read no longer yields
usable tokens.PdoUserRepository::findByProvider() now validates the
provider name against an allowlist before interpolating it into the JSON_EXTRACT path.OAuthManager::handleCallback() requires a matching state (an attacker can no longer
skip CSRF verification by omitting the parameter).plain removed. Only S256 is accepted in AuthorizationCode::verifyChallenge()
and advertised in the OIDC discovery document.Crypto::encrypt() / Crypto::decrypt() — authenticated encryption (AES-256-GCM) with an
enc:v1: version marker; decrypt() returns unmarked legacy values unchanged for safe
migration.token values in place (token = sha256(token)) to preserve active sessions.PdoTotpStorage. Existing plaintext secrets keep working (legacy passthrough) and are
encrypted on their next store(). Keep the master key stable — rotating it makes existing
encrypted secrets unreadable.How can I help you explore Laravel packages today?