Back to Feed
article 3m read

API keys moved to a hosted provider, and the scope check stayed behind as a word

After swapping my own key verifier for a hosted identity provider, every ingestion route still declares a required scope that nothing enforces, and revoked keys, rotation and attribution changed underneath it too.

Article DepthPractice

Flow

Six checks, one call, after the swap

  1. 1Key is real

    Now asked of the provider. Still checked.

  2. 2Revoked

    Provider knows, but each server's cache keeps accepting the key for a few minutes.

  3. 3Expired

    Left to the provider. My code no longer has its own answer.

  4. 4Allowed address

    Gone. The address is passed in and never read.

  5. 5Scopes

    Written to a debug log and ignored. Any valid key passes.

  6. 6Which key sent it

    Gone from incoming telemetry, because validation returns no key id.

Each ingestion route in my telemetry API still says the caller needs write access. Since I moved my API keys to a hosted login provider, nothing checks it.

One call, six checks

My old keys lived in my own table. When a key came in, my code checked six things:

  • Is the key real?
  • Is it revoked?
  • Is it expired?
  • Is it sent from an allowed address?
  • Does it have the scopes the route asks for? Scopes are the list of things a key may do.
  • Which key sent this, so the data can be tagged with it?

All six sat behind one function call, so when I swapped the call, all six went with it. The new code only handles the first one directly.

The scope list that checks nothing

The new code asks the provider if the key is valid. The routes still pass a scope list, and the code still accepts it. But the part that said no is gone. Nothing else uses the list.

The scopes a route asks for are only written to a debug log.

A missing guard is easy to see. A guard that names a permission looks safe to me, to a reviewer and to the type checker. That's what makes it worse.

Right now it does little harm. All new keys are the same kind, and the only routes using this guard all ask for write. The risk is the next route. If I add a read-only or admin route, it will look locked and let any valid key in.

The cache and revoked keys

Valid keys are cached in each server process for a few minutes. The cache is keyed by the secret itself. The revoke route only knows the key's id, so it can't clear the entry. A revoked key keeps working on each server until its copy runs out. Role and plan changes wait just as long.

I'd key the cache by a hash of the secret and keep a map from key id to hash, so a revoke can find and drop the entry. Or I'd keep the cache short and write that delay down as the promise.

Rotation and lost tracking

The old revoke route checked that the key belonged to the caller's organization. The new one hands the id straight to the provider, which might check that, though I haven't confirmed it.

Rotation now creates a new key and then revokes the old one. If the second step fails, the caller gets an error and never sees the new key, while the old key keeps working.

Incoming telemetry also no longer records which key sent it, and the address restriction disappeared as well.

Delete the option, or refuse to boot

Either the server refuses to start when a route passes scopes it can't check, or the option gets deleted. I lean toward deleting it, and moving admin routes to normal logins instead of keys.

None of my changes are written yet. And I can't promise this code ever ran. The notes in the same commit list type errors on the very provider calls it depends on.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward