Flow
Guards from lookup to hash
1Lookup
Returns false when the key is absent or empty.
2Signer and checker
Test for false before doing any hashing.
3Endpoint and login page
Return a server error, set no cookie, and tell the owner where to set a key.
4Shared signing function
Not built yet: throw when the key is missing or short, so a forgotten check is loud.
Crash was the staff scheduling app's one answer for a missing signing key.
That key signs the login tokens. The app looks for it in an environment variable, then in the config file, and an empty value counts as missing. When it found nothing, it threw an exception. The code that makes tokens and the code that checks them both asked for the key in the middle of their work, and neither handled the error. So a request that needed the key ended in a fatal error and probably a generic error page. Nothing got signed with a missing key, but nobody was told what to fix either.
Three branches in under an hour
The agent made three branches for this in under an hour, each bigger than the last. The first made the token checker say "not logged in" instead of crashing. The second did the same for the token maker, and made the sign-in endpoint return a server error with no cookie. The third stopped using an exception at all. Now the lookup returns false when there's no key, and the maker and checker test for that before doing any work. The login page checks for a key first and tells the person to set one. The README shows how to generate a random value.
All three branches also carried the same unrelated front-end edits. That's clutter in a change about secrets.
Where false turns into a key
An exception can't be ignored, but a false can. PHP will treat false as an empty string when a function wants text. So if any future code forgets to check, it signs tokens with an empty key, and anyone can make those. Login would look fine while being wide open.
The third version checks in the four places that use the key, and tests prove that making a token and checking a token both return false when the key is empty.
What's missing is a guard underneath all of them. The hashing step still accepts an empty key. If you're building something similar, put the check in one small signing function that throws when the key is missing or short:
if (strlen($key) < 32) throw new LogicException('no usable signing key');