Back to Feed
article 2m read

New login check read the token and never checked who signed it

An auth step I put in front of my agent-event collector decoded the bearer token instead of verifying it, so the caller's own claims picked the user, the organization, a default role and the author of every stored event.

Article DepthPractice

Flow

The order my token check should follow

Each step only uses what the step before it has already checked.

  1. 1What mine did

    Decoded the token and trusted the user and group.

  2. 2Check the token

    Signature with the provider's keys, audience, expiry.

  3. 3Read the user

    Only after the check passes.

  4. 4Require membership

    Not in the group means no, not a default role.

  5. 5Outage is not no

    A provider error should be a try-again.

  6. 6Author from the token

    Never from the event payload.

Logged-out visitors now get turned away by the event collector. But the check that turns them away never asks who signed the login.

The collector gets events from my coding-agent hooks. I added a login check in front of it. It takes the token from the request, reads the user and the group out of it, and passes them on. When I read it back, I saw it only decodes the token. It never checks it.

Decoding just unpacks text. Anyone can write that text. Checking means testing the signature with the login provider's keys, then making sure the token is for my service and hasn't expired. My code skipped all of that.

Where the caller's own words ended up

Those unchecked values decide three things:

  • whose account gets refreshed from the login provider,
  • which group the request acts in, with a member role handed out even when there's no membership,
  • whose name goes on each stored event.

A made-up user probably fails at the provider and gets turned away. A real user ID works, and those IDs aren't secret. So a fake token can borrow a real person, pick a group, and file events under their name. It can't create new memberships, because those come from the provider. When the membership lookup errors, the code just keeps going with the same default role.

A second change that day moved the provider's key into required startup settings. Good, but the decode didn't change.

The order it has to run in

const who = await verifyToken(token, providerKeys);

Check the token first. Only then read who it's for. If they aren't in the group, say no. Don't hand out a default role. Treat a provider outage as "try again", not as "no". Take the author of each event from the checked token, never from the payload.

When the collector answers with a 401, which means "not logged in", the command-line tool tells you to log in. It says your tools still work but nothing is saved until you do. That message is fine. The weak part is the server's 401, which a token with a real user ID in it still gets past.

Keep reading

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

5 paths forward