Let's start with a puzzle that sounds impossible.
In April 2022, someone quietly read private code from dozens of companies on GitHub — including npm, the giant that hosts code for millions of JavaScript projects. They did this without cracking a single password and without breaking any encryption.
Every lock worked perfectly. The thief just walked in the front door with a real key.
To understand how, we need to build up the idea one piece at a time.
Step 1: The problem OAuth was invented to solve
Imagine you use GitHub to store your code. You also use a service like Heroku to deploy that code to the internet.
For Heroku to deploy your code, it needs to read your code from GitHub. But how?
The naive answer is: "Give Heroku your GitHub username and password." That's terrible. Now a second company knows your password, can do anything you can do, forever, and if they get hacked, so do you.
What is OAuth?
OAuth is a system that lets you give one service limited permission to act on your behalf at another service — without sharing your password.
Think of a hotel valet key. You don't hand the valet the key to your house and your safe. You hand them a special key that only starts the car. That's the spirit of OAuth.
Here's the flow:
You GitHub Heroku
│ │ │
│ "Let Heroku │ │
│ read my repos" │ │
├───────────────────>│ │
│ │ issues a TOKEN │
│ ├───────────────────>│
│ │ │ stores token
│ │ │
│ │<── uses token ─────┤
│ │ to read code │
You approve once. GitHub hands Heroku a token. From then on, Heroku uses that token to fetch your code. Your password is never shared.
This is genuinely good design. Textbook-correct. Most engineers — including very experienced ones — would happily approve it.
Step 2: What exactly is a token?
What is a token?
A token is a string of characters that acts as proof: "the holder of this string is allowed to do X." It's like a movie ticket. Whoever holds the ticket gets into the theater. The theater doesn't check your ID — it just checks the ticket.
That last part is the whole story of this breach. Let's zoom in.
The GitHub tokens Heroku held were bearer tokens.
What is a bearer token?
A bearer token means whoever bears (holds) it can use it. No extra proof required.
Movie ticket = bearer token
→ whoever holds it gets in
→ the usher never asks "are you really the buyer?"
This is convenient. It's also the weak spot, because it has three dangerous properties combined:
- No proof of possession — the token doesn't prove the holder is really Heroku. It just proves the holder has the token.
- No expiry — it never ages out. A token issued in 2018 still works today.
- A huge scope:
scope=repo
What is a scope?
A scope defines how much a token can do. Think of it as the fine print on the valet key.
The scope on these tokens was scope=repo. In plain words:
scope=repo = read AND write access
to EVERY repository
the user can touch
Not one repo. Not read-only. Everything. Read and write, on every project you have access to.
So each token was a skeleton key — one key that opens every door — that never expires and only asks "do you hold me?"
Step 3: The attack — no cryptography was broken
Here's what actually happened.
The attackers did not break into GitHub. They broke into Heroku's own token store — the place where Heroku kept the tokens GitHub had given it.
Attacker
│
│ steals tokens from
▼
┌──────────────────────┐
│ Heroku's token store│ ← the tokens GitHub issued
└──────────────────────┘
│
│ now holds valid bearer tokens
▼
GitHub says: "Valid token! Come on in."
Once they had the tokens, the attackers just... used them. To GitHub, a stolen token looks exactly like a legitimate one, because a bearer token doesn't prove who's holding it.
Let's be precise about what still worked during all this:
- TLS held.
What is TLS?
TLS is the encryption that protects data traveling over the internet (the "s" in https). It scrambles data so no one can eavesdrop. It worked perfectly.
- Signatures verified. Every message that was supposed to be cryptographically sealed was sealed and checked correctly.
- Every cryptographic check passed.
So what broke? Not the math. An assumption.
The system assumed: "Whoever presents this token is Heroku, acting for this user." The attacker presented the token. The system believed the assumption. Game over.
Step 4: This is an old, old bug in disguise
This isn't a modern cloud accident. Security researchers have warned about this exact shape of failure for decades.
Ross Anderson, a famous security researcher, argued for years that protocols fail on freshness and naming — not on math.
What is "freshness"?
Freshness means: is this message/token new, or is it an old one being replayed? A token that never expires has no freshness. You can reuse a five-year-old one and nobody notices.
What is "naming"?
Naming means: does the system actually know WHO is on the other end? Not "does the holder have a valid ticket" — but "is the holder really the party we think it is?"
The classic proof came in 1995, when Gavin Lowe found a man-in-the-middle flaw in a well-known protocol called Needham-Schroeder — a full 17 years after it was published and trusted.
What is a man-in-the-middle attack?
A man-in-the-middle is an attacker who sits secretly between two parties, relaying messages, while both sides believe they're talking directly to each other.
A ───► [attacker] ───► B
◄─── ◄───
A thinks it's talking to B.
B thinks it's talking to A.
Both are wrong.
The Needham-Schroeder flaw and the GitHub token theft are the same class of bug: the protocol never truly established who was on the other end. The crypto was fine. The identity assumption was rotten.
Crypto proves the message is intact. It never proves who is holding it.
Step 5: How they fixed it — and why the fix hurt
The response came in two parts.
Part 1 — Heroku's cleanup. Heroku revoked every GitHub OAuth token it had ever issued.
What is revoking a token?
Revoking means telling the system "this token is dead — stop accepting it," even though it was valid before. Like calling the hotel to deactivate a lost key card.
Part 2 — GitHub's redesign. GitHub shipped fine-grained personal access tokens. Instead of the old skeleton key, you now get precise, limited keys:
OLD (dangerous) NEW (fine-grained)
────────────── ──────────────────
scope=repo contents: read
= read/write = read-only
= ALL repositories = one specific repo
= never expires = MUST expire
Two big changes stand out:
- Per-repository grants. A token can be limited to one repo, and to read-only (
contents: read) instead of full read/write. - Mandatory expiry. Tokens must age out. This directly fixes the "freshness" problem.
But here's the part most retellings skip: the fix genuinely hurt.
When Heroku revoked all its tokens, its GitHub integration went dark for roughly a month. Customers' automatic deploy pipelines broke along with it.
And expiring tokens created a new, ongoing chore:
Never-expiring token:
set it once → forget it forever (until you get robbed)
Expiring token:
must be rotated on a schedule
→ CI fails at 2am when a credential
quietly ages out
→ someone has to renew it
What is CI?
CI = Continuous Integration, the automated system that builds and tests your code every time it changes. If it uses an expired token, it fails — often at the worst time.
So security got safer but more painful. And that trade was accepted on purpose, eyes open.
A token that never expires is a skeleton key you've already handed out. The pain of rotating keys is cheaper than the pain of one you can never take back.
The core lessons
- OAuth's promise is real and good: give a service limited access without sharing your password. The design wasn't the problem.
- A bearer token trusts the holder, not the person. Whoever holds it can use it — like a movie ticket. If it's stolen, the thief is you, as far as the system knows.
- The most dangerous combination is: no proof of possession + never expires + huge scope. Any one of these is a risk; together they're a skeleton key.
- Crypto proves a message wasn't tampered with. It does NOT prove who's on the other end. Protocols fail on naming (who) and freshness (is this new?), not on math.
- This is an ancient bug — the same class Gavin Lowe found in Needham-Schroeder in 1995. New technology, identical mistake.
- Good fixes often hurt. Short-lived, narrowly-scoped tokens mean rotation schedules and occasional 2am failures. That ongoing pain is the price of never handing out a key you can't take back.
- Practical takeaway: go find your oldest never-expiring token. Give it an expiry. Give it the smallest scope it can survive on. Future-you will thank present-you.
