Skip to content
LaravelBlog

laravel/framework: Fix undefined array key in GenericUser::getAuthPassword() for passwordless users (#61876)

LaravelBlogBot
LaravelBlogBot

Contributed by tommie1001

Part of the framework v13.35.0 release

Category: Bug fix

Pull request #61876 fixes a small but noisy regression in the authentication layer: logging in a GenericUser that has no password attribute triggered an Undefined array key "password" warning on every login. The warning dates back to #61594, which taught SessionGuard::login() to store the user's password hash in the session. Logins still succeeded, but each one emitted a PHP warning that flowed straight into error trackers. If you're upgrading from v13.34.0, this patch makes GenericUser::getAuthPassword() return null instead of reading a missing array key, so passwordless logins are silent again — which is what the guard expected all along.

What changed

The patch is a one-line change in Illuminate\Auth\GenericUser:

// Before (v13.34.0)
public function getAuthPassword()
{
    return $this->attributes[$this->getAuthPasswordName()];
}

// After
public function getAuthPassword()
{
    return $this->attributes[$this->getAuthPasswordName()] ?? null;
}

The docblock return type widens from @return string to @return string|null. A new test, testLoginWithGenericUserWithoutPasswordDoesNotStorePasswordHash, covers the fix: it logs in a GenericUser created with only an id, installs a temporary error handler to capture warnings, and asserts that no warning fired, that the session still contains the user's ID, and that no password_hash_default key was written.

Why passwordless logins started warning

Since #61594, SessionGuard::login() calls getAuthPassword() on every login so it can store the password hash alongside the session ID. That is a security feature: the stored hash lets the guard invalidate the session when the user's credential changes.

GenericUser is the non-persistent user object that custom user providers commonly hand back — a thin wrapper around an arbitrary attributes array. For accounts that authenticate through an external identity provider (OAuth, SSO, API tokens), there is often no local password at all, so the password key simply is not present. The guard's unconditional call to getAuthPassword() performed a direct array read on that missing key, and PHP raised the warning before the guard could decide what to do with the value.

The result was easy to miss locally but painful in production: every request that logged a user in — for example, after session expiry — pushed one more Undefined array key "password" warning into Sentry, Bugsnag, Flare, or your log files.

The guard was already prepared for a missing password

The session guard's newer code paths already treat "no password" as a normal state:

  • login() only writes password_hash_{guard} when getAuthPassword() returns a truthy value;
  • #61532 handles a null password in userFromRecaller();
  • #61817 lets hashPasswordForCookie() accept null.

An Eloquent user whose password column is NULL already flowed through those paths without a peep, because Eloquent's getAuthPassword() returns null for a missing attribute. GenericUser was the outlier: it read the attribute directly and warned. With this patch, a GenericUser without a password behaves exactly like an Eloquent user with a null password column — the final piece that makes the passwordless case consistent across user implementations.

Example: logging in a passwordless GenericUser

use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\UserProvider;

class SsoUserProvider implements UserProvider
{
    public function retrieveById($identifier)
    {
        // Accounts authenticated through an external provider
        // have no local password credential.
        return new GenericUser(['id' => $identifier]);
    }

    // ...
}

// In a controller or middleware:
Auth::login($provider->retrieveById($request->route('id')));

// v13.34.0: emits "Undefined array key 'password'" on every login.
// With #61876: no warning, and no password hash in the session:
session()->has('password_hash_web'); // false

If your own code consumes getAuthPassword() directly, the return value can now legitimately be null:

if (($hash = $user->getAuthPassword()) !== null) {
    // the user has a local password credential
}

Upgrade impact

  • No config, migration, or code changes required. The fix is drop-in for anyone on 13.34.0: update and the warnings stop.
  • The only observable contract change is the return type. getAuthPassword() may now return null for a GenericUser (or a subclass) without the configured password attribute. Static analyzers such as PHPStan or Larastan may flag call sites that assumed a string — add a null check or ?? '' where appropriate.
  • Users with passwords are unaffected. When the attribute exists, it is returned unchanged, and the session hash is stored exactly as before.
  • Custom password names work the same way. If you override getAuthPasswordName(), the null fallback now applies to that key too.

Takeaways

  • GenericUser::getAuthPassword() now returns null when the password attribute is missing, instead of raising an Undefined array key "password" warning.
  • The warnings appeared because #61594 makes SessionGuard::login() call getAuthPassword() on every login to store a session password hash; passwordless GenericUsers tripped a missing-key warning each time.
  • Passwordless logins are now silent and store no password hash, matching Eloquent users with a null password column.
  • Upgrading from v13.34.0 is safe: no configuration changes, but treat the return type as string|null if you call getAuthPassword() in your own code.

Sources

More from this release

Related Articles

Chore 4 mins read

laravel/framework: Downloads no longer crash on filenames without an ASCII equivalent

**Category:** Bug fix (9) PR #61888 fixes an `InvalidArgumentException` thrown by `Storage::download()`, `Storage::response()`, `Storage::serve()`, and `response()->streamDownload()` whenever the download filename contains characters with no ASCII equivalent — for example `請求書` or `😀`. After this change, those methods emit a `Content-Disposition` header with one underscore per character as the ASCII fallback, matching the behavior `response()->download()` already had via Symfony's `BinaryFileResponse`. The fix is two small edits to `fallbackName()` plus test coverage, and it should be a painless upgrade from v13.34.0. ## What broke When Laravel builds a download response for a non-ASCII filename, it produces both parameters of an RFC 5987/6266 `Content-Disposition` header: a plain `filename=` fallback for legacy clients, and `filename*=utf-8''...` carrying the real name percent-encoded. That fallback comes from `fallbackName()`, which pipes the name through `Str::ascii()`. For names that transliterate cleanly, this works: `привет.txt` becomes `privet.txt`. But some characters have no ASCII mapping at all. `Str::ascii('請求書')` returns an empty string, and the disposition logic then rejects the empty (or original, non-ASCII) fallback: ```php Storage::put('file.txt', 'Hello'); return Storage::download('file.txt', '請求書'); // InvalidArgumentException: // The filename fallback must only contain ASCII characters. ``` Meanwhile `response()->download()` already survived this exact input, because Symfony's `BinaryFileResponse` substitutes one `_` per character when the fallback comes back empty. The streamed and Storage paths had no such safety net, so the framework behaved inconsistently depending on which download method you picked. ## The fix Both `fallbackName()` implementations — in `Illuminate\Filesystem\FilesystemAdapter` and `Illuminate\Routing\ResponseFactory` — now check whether transliteration left anything behind, and substitute underscores if it didn't: ```php protected function fallbackName($name) { $fallback = str_replace('%', '', Str::ascii($name)); return $fallback !== '' ? $fallback : str_repeat('_', mb_strlen($name)); } ``` Note the use of `mb_strlen()`: the underscore count matches the character count of the original name, so `請求書` (three characters) yields `filename=___`, and a single emoji yields `filename=_`. The existing `str_replace('%', '', ...)` behavior — stripping percent signs so the fallback can't be mistaken for an encoded sequence — is untouched. ## Consistent headers across every download method After the fix, all four previously crashing paths produce the same header as `response()->download()`: ```php // Before response()->download($path, '請求書'); // filename=___ response()->streamDownload($cb, '請求書'); // Crash Storage::download('file.txt', '請求書'); // Crash // After — all three // attachment; filename=___; filename*=utf-8''%E8%AB%8B%E6%B1%82%E6%9B%B8 ``` The `filename*` parameter still carries the exact UTF-8 name, so modern browsers continue showing `請求書` in the save dialog. The underscores exist purely as the legacy `filename` fallback for clients that don't understand `filename*`. Critically, the fallback only engages when transliteration produces nothing at all. Names that already work don't change: `привет.txt` still becomes `privet.txt` (not `______.txt`), and `naïve résumé.txt` still becomes `naive resume.txt`. ## Test coverage Two new tests lock in the exact header. `FilesystemAdapterTest::testDownloadFilenameWithoutAsciiEquivalent` covers `Storage::download()`, and a brand-new `RoutingResponseFactoryTest` covers `streamDownload()`: ```php $response = $factory->streamDownload(fn () => null, '請求書'); $this->assertSame( 'attachment; filename=___; filename*=utf-8\'\'%E8%AB%8B%E6%B1%82%E6%9B%B8', $response->headers->get('content-disposition') ); ``` ## Upgrade impact For anyone upgrading from v13.34.0, this is a drop-in bug fix. There are no method signature changes, no config changes, and no new dependencies. The only behavioral difference is for filenames that previously threw the exception. If you worked around the crash — catching the `InvalidArgumentException`, or pre-sanitizing names with something like `Str::ascii($name) ?: 'download'` — you can remove that code and pass user-supplied filenames straight to the download methods. **Takeaways:** - `Storage::download()`, `Storage::response()`, `Storage::serve()`, and `response()->streamDownload()` no longer throw for filenames with no ASCII equivalent. - Empty transliteration fallbacks now become one `_` per character, matching Symfony's `BinaryFileResponse` and `response()->download()`. - Names that transliterate (e.g. `привет.txt` → `privet.txt`) behave exactly as before, and `filename*` still carries the true UTF-8 name. - Safe upgrade: no API or config changes; drop any try/catch workarounds you had around the old exception.

LaravelBlogBot
LaravelBlogBot