The trick I've used for this is to have tokens that look like this:
"234523:7a561002f780e23853bdfbd89ae79bf2"
Then you have database entries like this:
id = 234523 token = 7a561002f780e23853bdfbd89ae79bf2
When a token comes in you use the bit before the : to look up the database record by its indexed ID, then perform a safe comparison between the rest of the token and the value you retrieved from the database.
That way you’re hashing the user input, which sanitizes it. It also protects against timing attacks.*
And most importantly it protects against credentials leaking. Even insiders who can read a token from the database can’t use it to authenticate, because the app logic expects a pre-image of that value.
*HMAC resists timing attacks because the attacker can no longer control the bit pattern on either side of the equality check.
Semantically these are very different objects/concepts
Passwords:
- tightly couple authentication (I am kortex) and authorization (I can do everything logged-in kortex can do)
- not meant to be rotated unless breached (forced password rotation mandates weaken passwords, imho)
- are passwords meant for human transport, if even briefly
- hard to revoke (once expired, still valid until user rotates)
- usually insufficient entropy, dependent on user habits
- one flavor: a secret string
Tokens:
- decouple authentication and authorization
- ephemeral
- revokable
- enable automatic rotation (session tokens vs auth tokens)
- can have metadata
- as much entropy as you want
- many flavors
If there were one login system/site in the whole world, you'd still want to hash/encrypt them.
I mean hashing is one of the easiest things to do to add a layer of security, so even if the db is encrypted, why not? Maybe I'm missing your point.
Totally agree with your main point of "tokens, like passwords, should be kept discrete."
Passwords are secrets.
Ed25519 private keys are secrets.
We hash passwords.
Thus we must hash tokens.
Should we therefore hash our Ed25519 private keys?
No, but I'm of the opinion that you should encrypt private keys before storing them in a database.
The only thing the post explains that there is a difference between checking a secret with the database vs message validation, also based on a secret. But for both, if you have the secret, you're doomed.
Expiration / immediate revocation when doing message validation is more difficult than having a distributed cache, which supports key eviction/expiry/removal.
Also, an asymmetric solution has similar problems: if you get the private key from the client, you're doomed. So it does protect the server part, but if people have access there, you're even more doomed. But at least it'll be easier to identify who had a security leak./
Passwords are usually one-per-resource, e.g. a user has a single password. If that password is compromised you can reset it, but all the consumers that used it will need the new credential.
Whereas tokens are typically one-per-consumer, so if one is compromised you can revoke/expire just that token, without affecting other consumers.
Same with expiry times -- you can set that on a token without it expiring all access to that resource.
Not sure if that's what the author had in mind, but it's a difference in how these things are often used (even if fundamentally they are otherwise very similar).
Frustratingly, different tokens have different levels of exposure to all of these problems. A well-confined Macaroon has essentially none of the secret storage problems of a password. An API request authenticating key has none of the bearer token problems. No API token has the human-recall, reuse, and brute-force problems.
People hyperfixate on the secret storage problem, I think because they feel like they can get their heads around it. As you can probably tell, I'm loath to relitigate the debate about whether we should store passwords with secret salts. Even when the discussion is legitimately focussed on that problem, I find discussions about it go to weird, incoherent places that aren't really informed by real threat models, and are really hair-splitting arguments about the security of environment variables vs. the security of filesystems, just dressed up as top-3 security considerations.
Even for trivial basic-auth API keys, the secret storage problem is not the same as that of a password. Part of the problem with passwords is that they effectively cannot be revoked; "revoked" passwords creep back into systems, and, worse, infect other systems. That's not how API keys work; the threat model is different and so are the countermeasures that are profitable to deploy for them.
Another thing that I feel happens in discussions like this is that there's no notion of cost-benefit. There is option A, and option B, and option B is on some axis superior to option A, even if that benefit is marginal. Since there's no engineering cost to consider, there's no meaningful discussion. But in reality, there's always a cost: deploying a countermeasure at a minimum incurs the opportunity cost of not deploying some other countermeasure that could have been built with the same (finite) engineering resources. Since these discussions always seem to get mired in secret-salt double-hashing hmac(hmac(root-secret, measurement_1 || measurement_2), secret) stuff, I hope you can see the concern: the discussion essentially advocates for ever-more-marginal wins that "fit" the message board thread, rather than seriously considering the real problem.
But of course, the other problem is that the arc of the thread bends towards HMAC'ing your Biscuit token. "Tokens are just passwords", after all.
At any rate: this thread says, "passwords are secrets, tokens are secrets, ergo passwords are tokens". Signing keys are also secrets. Nobody HMACs them. Something is wrong with the syllogism. I'm not that interested in picking apart what.
It can, but the practicality of exploiting this timing leak isn't at all a settled issue.
Previously, https://soatok.blog/2021/08/20/lobste-rs-password-reset-vuln...