Flow
Tokens the new checks should refuse
None of these has a test yet.
1Bad signature
Made-up claims signed with a key the provider never used.
2Wrong algorithm
Header asks for a weaker algorithm, or none.
3Lookalike issuer
Starts like the provider's host, ends somewhere else.
4Plain-http issuer
Right host name, unsafe scheme.
5Expired token
Real signature, past its time.
Any login token that looked right was trusted by the API, because it read the token without checking who signed it.
People log in through a hosted provider and send the API a short-lived token. My code split that token, decoded the middle, read a user and an organization, and trusted them. Real tokens worked fine, so it looked done.
Reading the token versus checking it
A signed token has a header, the claims, and a signature. Decoding only reads the claims. Checking the signature against the provider's public key is what proves the provider made it. With a real token, both give the same answer. With a fake one, only the check catches it. My code skipped the check:
const claims = JSON.parse(atob(token.split(".")[1])); // no signature check
So anyone who could write a bit of JSON could claim to be any user. I saw no sign that anyone did. That's just what the old code allowed.
Believing the token only after the signature checks out
The API now checks every token against the provider's published public keys. It loads them once and reuses them. I'm counting on the library to pick up new keys when the provider changes them, but I haven't watched that happen.
It also accepts just one signing algorithm, so a token can't tell the server to use a weaker one, or none. The user and organization now come only from the checked token.
Parsing the issuer instead of matching strings
The token also has to say it came from the provider. Matching the start of a string is not enough, since a fake host can start with the real name and end with someone else's. So the API reads the issuer as a web address and compares the host name exactly. If anything fails, the API logs a warning and gives back no identity.
Two things review caught that I didn't
My first version never checked for https. Review spotted that, and plain http is now refused.
The same change locked down which websites can call the API from a browser. It used to allow every site, with logins attached. Now production only allows a list from config. If nobody fills that list in, every browser call gets blocked, which would look like an outage. So the API now logs a loud warning at startup when the list is empty.
Gaps the checks still leave
There's no check that a token was meant for this app, and no forged-token test yet. And there's no way to cancel a token. If one gets stolen, it works until it runs out.