Understand why a trusted service can be tricked into fetching the wrong user's data, then design the second credential that carries the user — a short-lived, audience-scoped token nobody can mint for themselves.
Learning objectives
Describe the confused deputy problem with a concrete request.
Explain why a service cannot safely sign a claim about someone else.
Read the six claims inside a JWT and say what attack each one prevents.
Choose asymmetric signing over shared-secret signing and justify it.
A story
A user logs into your web application. Their browser calls your BFF — the backend for frontend, the service that exists to serve one user interface. The BFF calls the backend to fetch order 12345.
After Lesson 7, the backend knows exactly one thing about that call: the BFF is calling. Nothing else.
So if the BFF has a bug — it forgot to check that order 12345 belongs to this logged-in user — the backend returns someone else's order without hesitation.
The backend cannot help. It was never told who the user is.
The name for this
A trusted service doing untrusted work on someone else's behalf is called the confused deputy. The deputy is not malicious; it is confused. It is one of the most common serious bugs in multi-service systems, and no amount of certificate work will find it.
Six words to fix before we go further
The next three lessons implement a piece of OAuth, and OAuth has a vocabulary that is worth pinning down now. Every one of these appears in the code you are about to write.
Word
Means here
Authorization server
The auth server. The only party that mints tokens
Client
A service asking for a token. Your Go and TypeScript services
Resource server
A service being called, which must decide whether to comply
Subject token
The opaque session string a client offers in exchange
Access token
The short-lived JWT it gets back
Delegation
Acting for a user, with both parties named in the token
One distinction underneath all of them, and it is the one people blur:
Authentication is not authorization
Authentication answers "who is this?". Authorization answers "may they?". Everything through Lesson 7 was authentication. A verified identity does not decide anything on its own; it just gives you something to decide about.
Keep that separation in view, because a system can authenticate perfectly and still hand out the wrong data — which is exactly the story above.
From /health to something worth protecting
Up to now the two services have called each other's /health. That was the right choice while the question was "can they talk at all", and it is the wrong choice now: a liveness probe is not acting for a user, and a load balancer should never need a token to ask whether a process is alive.
So from Lesson 10 the services keep /health as a tokenless probe — still mTLS-authenticated, but not acting for a user — and add a route that actually holds data:
the two routes
GET /health no token. is this process alive?
GET /orders/{id} token required. and then three more questions
That second route is where the rest of this chapter lives, because it is the first route where "who is calling" is not enough of an answer.
The fix: two credentials
Every call is going to carry two things, answering two different questions:
Credential
Answers
Built in
mTLS certificate
Which service is calling?
Lesson 7
Short-lived token
Which user is it for?
Lessons 9 and 10
And — the part people skip — the receiver checks that the two agree.
Why a whole third program?
Sit with this question, because it is the reason for everything in the next two lessons.
Why can the Go service not simply sign its own tokens?
Because then it could name any user it liked — "user": "anybody" — and nobody could tell.
A claim you make about yourself can be checked against something. A claim you make about someone else — "I am acting for Alice" — is worth nothing unless a third party that both sides trust vouches for it.
So minting moves to an auth server. And crucially, that server reads the caller's identity from their certificate, never from what they send in the request.
Flow· Who says what
Go service ──mTLS──► auth server
"swap this user session for a
token aimed at the TS service"
auth server reads the ACTOR from
the certificate, not from the body
│
▼
token: sub = the user
act = the calling service
aud = the one service that may accept it
│
Go service ──mTLS + token──► TS service
What a JWT is
A JWT — JSON Web Token — is three chunks of base64 joined by dots:
It was not encrypted. Base64 is an encoding, not encryption — it hides nothing and requires no key to undo. Anyone holding a JWT can read every claim inside it.
What stops you editing it is the third chunk. Change one byte of the claims and the signature no longer matches.
The rule
A JWT is tamper-evident, not private, and the difference matters. Nothing stops someone editing the payload — the bytes are theirs to change. What they cannot do is produce a matching signature, so the edit is caught the moment you verify. Never put a secret inside one, and never assume a user cannot read their own token.
That sentence is the whole chapter. A certificate can say the first half. Only a token from a trusted third party can say the second.
If the claim names are not sticking yet, picture a visitor badge printed at a building's reception desk:
On the badge
Claim
Meaning
Issued by: Reception, Tower A
iss
Who printed it
Visiting: Floor 9 only
aud
The one place it opens
Visitor: Alice
sub
Who the visit is for
Escorted by: Bob, staff
act
Who is walking her in
Access: meeting rooms
scope
What it permits
Expires: 17:00
exp
When it stops working
Reception prints the badge because reception is the only party both Alice and Floor 9 trust. Bob cannot print his own badge saying he is escorting Alice, and a badge for Floor 9 does not open Floor 4. That is the entire design.
Why aud and exp matter so much
They shrink the damage from a leak.
Without aud, a token stolen from one service works against every service. With it, a stolen token is useful against exactly one.
Without exp, a token stolen last year still works. With sixty seconds, an attacker has less than a minute to use it — and cannot store it for later.
Neither claim prevents theft. Both make theft much less valuable, which is usually the realistic goal.
Two ways to sign, and why we pick one
There are two families of signature, and the difference is how many keys are involved.
HMAC, written HS256 in a token header, uses one shared secret. The same string both creates and checks the signature — which should sound familiar, because it is the Lesson 4 problem wearing a different hat.
ECDSA, written ES256, uses a key pair, exactly like the certificates in Lesson 5. The private key signs; the public key only checks. This is the same elliptic-curve maths your certificates already use, applied to a token instead of a connection.
HMAC (HS256)
ECDSA (ES256)
Keys
One shared secret
Private key signs, public key verifies
Verifier can also forge?
Yes
No
Distributing the verify key
Must stay secret
Safe to publish
We use ES256.
With HMAC, every service that needs to check tokens must hold the very key that creates them. A single compromised backend could then forge a token for any user in the system. With ES256 a compromised backend can only check, because it never holds signing material at all.
Design rule
Prefer asymmetric signing whenever you have the choice. The question to ask is not "is this key safe?" but "how many machines have to hold it, and what can each of them do with it?"
Three things not to do
These are the mistakes this design exists to prevent. They are worth reading before you write any code, because all three are common and all three look reasonable.
Do not forward the user's original login token downstream. It was issued for the BFF, it often lives for an hour, and every service that touches it can then impersonate that user everywhere. Exchange it for a narrow, short-lived, single-audience token instead.
Do not pass a user id in a header such as X-User-Id: 42. Lesson 7 already covered why: a header is a claim, not proof. Anyone who can reach the port can write the same header.
Do not call downstream with a service account and no user context. That is precisely the confused deputy you are trying to fix, formalized into an architecture.
What you will build next
Lesson
You build
9
The auth server: mints tokens and publishes its public key
10
The client that requests and caches tokens, and the verifier that runs six checks
The auth server is a third Go program in the same repository. It requires mutual TLS, so it can identify the caller from a certificate, and it holds a signing key that nobody else has.
Lesson 8 checkpoint
You are ready for Lesson 9 when you can explain all of these without rereading:
what the confused deputy problem is, using a concrete request;
why a service cannot be trusted to sign a claim about a user;
why a JWT is readable by anyone who holds it;
what iss, aud, sub, act, scope, and exp each do;
what attack aud prevents that exp does not, and the reverse;
why ES256 is a better fit here than HS256.
Next you will write the auth server, sign your first token by hand, and decode your own claims out of it.