« Phase 08 · Track Overview

Warmup — Agent & Workload Identity, From Zero

Assumes HTTP and a working knowledge of hashing. Assumes nothing about OAuth, OIDC, JWTs, SPIFFE or token exchange. This is the densest phase in the track and the one that most repays reading slowly.


Table of Contents


1. Why agent identity is a new problem

Start with the thing that does not work, because everyone builds it first.

The service account. Give the agent fleet one credential with the union of every permission any agent might need. It is one line of configuration and it works immediately.

Then read the audit record it produces:

2026-03-12T14:22:03Z  actor=svc-ai-platform  action=payments.release  amount=250000

Who released the payment? "The platform." Which user asked? Unknown. Was that user entitled to release AED 250 000? Nobody checked — the service account was. If the credential leaks, an attacker has every permission every agent has, forever, and revoking it stops all agents at once.

Now enumerate what makes an agent different from a service:

PropertyConsequence
It acts on behalf of a personits authority must be bounded by that person's, not by its own
It acts across multiple hopsauthority must propagate and narrow at each one
It discovers tools at runtimeyou cannot enumerate its permissions at design time
It delegates to other agentspossibly across an organizational boundary
There are hundreds of themand non-human identities already outnumber humans in most enterprises by a large multiple

Every row breaks the service account. The mechanisms that survive are the four properties this phase builds, and each is a control an examiner will ask you to demonstrate:

Derived — from a verified assertion, never asserted by the caller. Narrowed — audience and scope shrink at every hop. Chained — the actor list is append-only and visible. Short-lived — seconds to minutes, so revocation is a timeout.

2. Authentication, authorization, and the words in between

Four words people use interchangeably and should not:

  • Authentication (AuthN)who is this? Verifying an identity claim.
  • Authorization (AuthZ)may they do this? A decision about a specific action.
  • DelegationA acts on behalf of B, and both are visible.
  • ImpersonationA becomes B; the fact that it was A is erased.

The last two are the phase's central distinction, and §7.2 covers it properly.

Two more terms you need:

  • Principal — the identity a decision is about. For an agent flow this is a composite: "agent A acting for user U", and both halves constrain what is allowed.
  • Non-human identity (NHI) — any identity that is not a person. Services, workloads, bots, and now agents.

3. Tokens

3.1 The JWT structure

A JSON Web Token is three base64url segments separated by dots:

eyJhbGciOiJIUzI1NiIsImtpZCI6ImsxIn0 . eyJpc3MiOiJodHRwczovL2xvZ2luIn0 . 3f9a2c...
└────────── header ──────────────┘   └────────── payload ──────────┘   └ signature ┘

Headeralg (the signing algorithm), typ, and kid (which key signed it, so a verifier can select from several during rotation).

Payload — the claims (§3.2).

Signature — over base64(header) + "." + base64(payload). The signature covers the encoded form, which is why you must never re-serialize before verifying: JSON key order is not canonical, and a re-encode changes the bytes.

Base64url, not base64: - and _ instead of + and / so it is URL-safe, and padding is stripped. That last detail is a genuine trap — you must restore the = padding before decoding, and forgetting it fails on exactly two thirds of otherwise-valid tokens. The lab tests every padding case for this reason.

A note on the lab's _json_b64: it serializes with sorted keys and no spaces. JOSE does not require canonical JSON; a deterministic test does. Without it the same claims produce different tokens on different Python versions.

3.2 The claims, and who checks each one

ClaimMeansChecked byAttack if skipped
ississuerresource servera token from a rogue issuer is accepted
subthe principaleveryonewrong attribution in every audit record
audwho it is forresource serverthe confused deputy — §3.4
exp / nbf / iatvalidity windowresource serverreplay of an expired credential
jtiunique idreplay cachea one-time token used twice
scopewhat may be doneresource serverexcessive agency
actthe delegation chaincontrol plane, audityou cannot tell agent-for-user from user
cnfproof-of-possession bindingresource servera stolen bearer token works
client_idwhich client obtained itaudityou cannot trace the entry point

The claims nobody checks are the vulnerabilities. A verifier that validates the signature and stops has confirmed the token is authentic and nothing about whether it is for you, still valid, or sufficient.

3.3 The two classic JWT breaks

alg: none. The JWS spec includes an "unsecured" mode with no signature. A verifier that reads the algorithm from the token and dispatches on it will happily accept a token with no signature at all — because the attacker set alg to none and the verifier obeyed.

Algorithm confusion. A verifier configured for RS256 (asymmetric) is handed a token signed HS256 (symmetric) using the public key as the HMAC secret. The public key is public, so the attacker can forge freely, and a naive library that picks the algorithm from the header verifies it successfully.

Both have the same root cause and the same fix:

The verifier decides the algorithm. The token never does.

The lab's verify_signature compares header["alg"] against the signer's configured algorithm and refuses a mismatch — and it uses hmac.compare_digest, because a short-circuiting == on the signature leaks it one byte at a time to an attacker who can measure response times.

3.4 Audience, and the confused deputy

The confused deputy is a privileged intermediary tricked into using its authority for someone else. In token terms:

Service B holds a valid token that a user presented to B. B sends that same token to service C. If C does not check aud, C accepts it — and B has just used the user's authority against a service the user never intended.

In an agent platform this is not hypothetical: agents call each other constantly, and forwarding the incoming token is the obvious implementation. It is also why every hop must exchange rather than forward (§7).

aud validation is the defence: a token minted for agent-platform is refused by core-banking, full stop, regardless of how valid its signature is. It is the single most important check in §3.2's table, and it is the one most commonly missing.

4. OAuth 2.0 → 2.1

4.1 The four parties

PartyIs
Resource ownerthe human who owns the data
Clientthe application acting on their behalf (your channel, your agent)
Authorization servermints tokens (Entra ID, in a bank)
Resource serverthe API holding the data (core banking)

OAuth exists so the client never sees the user's password: the user authenticates to the authorization server, which issues the client a scoped, expiring token.

4.2 The authorization code flow

user → client:  "look at my payments"
client → AS:    /authorize?client_id&redirect_uri&scope&code_challenge   (browser redirect)
AS → user:      authenticate, consent
AS → client:    redirect back with a CODE                                (not a token)
client → AS:    /token   code + code_verifier + client_id                (back channel)
AS → client:    access token (+ ID token, + refresh token)
client → RS:    Authorization: Bearer <access token>

The code is a one-time, short-lived, single-use reference. It travels through the browser (where things leak — history, referrers, logs); the token travels only on the back channel.

Three properties the lab enforces:

  • Single use. A redeemed code is dead. The lab marks it used before checking expiry, so a replay is unambiguous.
  • Exact redirect-URI matching. Prefix matching turns an open redirect into a code-interception attack. OAuth 2.1 requires exact.
  • Client binding. A code issued to client A cannot be redeemed by client B.

4.3 PKCE, derived

The attack. A public client (a mobile app, a SPA) has no secret. If an attacker can intercept the redirect — a malicious app registering the same custom URI scheme, say — they get the code and can redeem it, because redemption needs only the code and a public client_id.

The fix (RFC 7636). Before starting, the client generates a random code_verifier and sends its hash:

$$\text{code_challenge} = \text{base64url}(\text{SHA-256}(\text{code_verifier}))$$

The authorization server stores the challenge with the code. At redemption the client presents the verifier; the server hashes it and compares. An attacker with the intercepted code does not have the verifier and cannot derive it from the challenge (that is the preimage resistance of SHA-256).

Why S256 only. RFC 7636 also defines plain, where the challenge is the verifier. That protects against nothing: an attacker who can intercept the code can intercept the challenge, and the challenge is the verifier. OAuth 2.1 removes plain, and the lab raises on it.

Why PKCE is now mandatory for confidential clients too. A client secret protects against a different attacker (one who cannot see the redirect but can call the token endpoint). PKCE protects against code interception. They are orthogonal, and OAuth 2.1 requires both.

4.4 What OAuth 2.1 removed, and why

OAuth 2.1 is a consolidation, not a new protocol. The removals are the interesting part:

RemovedWhy
Implicit grantreturned tokens in the URL fragment, where they leak via history, referrers and logs
Resource-owner password grantthe client sees the user's password, which defeats OAuth's entire purpose
Bearer tokens in query stringsURLs are logged everywhere
plain PKCEprotects against nothing (§4.3)
Prefix redirect-URI matchingopen redirect → code interception

And the additions: PKCE mandatory for all clients, and refresh tokens must be either sender-constrained or one-time-use with rotation.

Refresh-token rotation is worth knowing even though the lab omits it: each use returns a new refresh token and invalidates the old one. If an old one is reused, that is evidence of theft — someone has a copy — and the correct response is to revoke the entire token family, not just that token. That inference-from-reuse is a genuinely elegant piece of design.

4.5 Client credentials, and why it is the wrong default

The client-credentials grant is machine-to-machine: a client authenticates with its own secret and receives a token representing itself. No user involved.

It is correct for genuinely user-less work — a nightly batch, a health check.

It is wrong for an agent acting for a person, and the lab shows why by printing both audit chains side by side:

client credentials:  batch-runner
delegation:          u-42 -> orchestrator -> payments-investigator

The first cannot answer "who asked?", cannot be bounded by what the user is personally entitled to do, and attributes every action to the platform. Reaching for client credentials because it is simpler is the single most common wrong turn in this phase.

5. OIDC: the identity layer

OAuth 2.0 is about authorization — it says nothing about who the user is. OpenID Connect adds that as a thin layer, and its contribution is one artifact:

The ID token — a JWT about the user, audienced to the client.

Access tokenID token
Aboutwhat may be donewho the user is
Audiencethe APIthe client
Consumed bythe resource serverthe client application
Containsscopessub, name, email, auth_time, …

Never send an ID token to an API. Its audience is the client; an API accepting it is failing the check in §3.4. Conversely, never inspect an access token in the client to learn who the user is — that is what /userinfo and the ID token are for, and an access token's format is not guaranteed to be readable.

This confusion is why people say "OIDC and OAuth are the same thing". They are not: one issues authority, the other issues identity, and mixing the artifacts breaks audience validation.

6. Scopes and the narrowing algebra

A scope is a string naming a permission: payments.read, crm.write.

Three things that are not the same and are constantly conflated:

  • Requested scope — what the client asked for.
  • Granted scope — what the authorization server issued (⊆ requested, and ⊆ what the client is registered for).
  • Effective permission — granted ∩ what the user may do ∩ what policy allows right now.

An agent should receive the task's scope, not the user's scope. A user entitled to release payments does not mean an agent answering a question for them should hold payments.release. Down-scoping at the boundary is the mechanism, and it is the practical form of least privilege in this phase.

The narrowing algebra. The lab makes escalation structurally impossible:

def narrow_scopes(held, requested):
    return normalize([r for r in requested if any(covers(h, r) for h in held)])

This function cannot return a scope outside held, for any input. That is a much stronger guarantee than "we check for escalation", because there is no code path that grants.

require_no_escalation adds the second rule: refuse rather than silently drop. Returning a smaller scope than requested lets a caller proceed believing it has authority it does not, and fail later at a point far from the cause. An explicit error at the boundary is worth a great deal.

The lab supports one wildcard form (payments.*). Deliberately limited — a scope language with real pattern matching becomes a policy engine, and then nobody can tell what a credential permits by reading it. Policy belongs in Phase 09; scopes should stay legible.

7. Token exchange (RFC 8693)

7.1 The problem it solves

User U's token is audienced to the agent platform. The platform's orchestrator must call the investigation agent, which must call core banking. Three options:

  1. Forward the token. The confused deputy (§3.4). Core banking either rejects it on audience — correct — or accepts it, which is worse.
  2. Use a service account. The user disappears (§4.5).
  3. Exchange it. Trade the token for a new one with a different audience, a narrower scope, and the actor recorded.

RFC 8693 standardizes the third. It is the backbone of multi-hop agent identity, and it is what makes Phase 03's delegation chain unforgeable.

7.2 Delegation versus impersonation

RFC 8693 supports both, and the difference is whether the chain survives.

Delegation — the new token keeps the user as sub and records the agent in act:

{ "sub": "u-42",
  "act": { "sub": "payments-investigator",
           "act": { "sub": "orchestrator" } } }

Note the nesting: the outermost actor is the most recent. That is fixed by the RFC and it is the opposite of what most people assume — worth checking before you read a chain during an incident.

Impersonation — the new token has the agent as sub and no chain. Downstream, the request looks like the agent acting alone. The user is gone.

For a regulated platform the choice is not close. Delegation preserves the answer to "on whose behalf?", which is the question every audit asks. The lab disables impersonation by default and requires an explicit flag to enable it — and the test asserts that enabling it erases the chain, which is the point.

7.3 The four rules of an exchange

Every exchange must:

  1. Narrow the audience. The new token is for a more specific service. An exchange returning the same audience has achieved nothing and is refused.
  2. Subset the scope. Never widen. §6's algebra makes this structural.
  3. Append to the chain — and refuse a cycle. If the actor is already in the chain (or is the subject), you are forming A→B→A: a loop across owners that nobody can see whole.
  4. Shorten the lifetime. min(requested, policy max, the parent's remaining life). A derived credential that outlives its parent is a privilege escalation in the time dimension — the user's session ends and the derived credential keeps working.

Plus a precondition the lab enforces with a may_delegate claim: not every token may be exchanged. A credential minted for a leaf tool should not be tradeable onward, and marking that explicitly is cheaper than reasoning about it later.

7.4 Walking three hops

From the lab's worked example:

HopAudienceScopeChainLife
user tokenagent-platformpayments.read, payments.release600 s
→ orchestratorpayments-investigatorpayments.read, payments.releaseu-42 → orchestrator120 s
→ investigatorcore-bankingpayments.readu-42 → orchestrator → payments-investigator≤ 118 s

Read the last row as core banking sees it: user u-42 asked, the orchestrator delegated, the investigator is acting, it may only read, and this credential dies in under two minutes.

That is a sentence you can put in front of an examiner. The service-account alternative is "the platform did something."

8. Workload identity: SPIFFE and SPIRE

Everything above authenticates a user through a client. A separate question: how does a running process prove what it is?

The traditional answer is a secret in the environment — which must be provisioned, rotated, protected, and which is copyable. The population of such secrets in a bank is enormous and mostly unmanaged.

SPIFFE (Secure Production Identity Framework For Everyone) inverts the direction of trust:

The workload presents nothing. The platform observes properties it can verify — this pod, this namespace, this service account, this image — and issues an identity based on them.

Three concepts:

The SPIFFE ID — a URI naming a workload: spiffe://bank.ae/ns/agents/sa/investigator. The authority is the trust domain; the path is the workload.

The SVID — the credential carrying the ID, as X.509 or JWT. Short-lived (minutes) and rotated automatically by the infrastructure. There is no secret to manage because there is nothing durable to steal.

Attestation — how the platform verifies. Node attestation proves the machine (a cloud instance-identity document, a TPM); workload attestation proves the process on it (its namespace, service account, image digest). SPIRE is the reference implementation.

Two rules the lab enforces, and both matter:

  • All registered selectors must match. A registration for ns=agents, sa=investigator must not be satisfied by a workload presenting only ns=agents — a subset match lets any workload in the namespace claim a narrower identity.
  • Ambiguity is refused, not resolved. If two registration entries match, the correct answer is an error. Guessing assigns identity nondeterministically, which is the worst possible property for an identity system.

Federation across trust domains is explicit. Two SPIFFE domains do not trust each other by default; a federation relationship is configured, and the lab's mtls_authorize refuses a foreign caller unless its domain is federated and it is allow-listed.

9. Sender-constrained tokens

A bearer token is authority in a string: whoever holds it, wields it. Steal it and you are the principal until it expires.

A sender-constrained token is bound to a key the holder must prove possession of. Stealing the string is no longer enough.

Two mechanisms:

  • mTLS-bound (RFC 8705) — the token carries a thumbprint of the client's TLS certificate in cnf. The resource server checks that the presenting connection used that certificate.
  • DPoP (RFC 9449) — the client signs a small JWT per request with a private key whose thumbprint is in cnf. Works without mTLS, which suits browsers and mobile.

The lab implements the cnf check generically: a token carrying a thumbprint is refused unless the presented key matches. The test — "stolen token, wrong key, refused" — is the whole argument.

When is it worth the complexity? For short-lived credentials in a controlled network, bearer is often acceptable, because a 60-second token has little value. For anything crossing a boundary, anything long-lived, or anything that moves money, binding is the difference between "theft is sufficient" and "theft is not sufficient."

Note the interaction with §8: if your workloads already have SVIDs and mTLS, sender-constraint is nearly free — the certificate is already there. That is a good reason to do SPIFFE first.

10. Non-human identity as a lifecycle

An agent is an identity, and identities have lifecycles. The lab's:

registered ──► approved ──► active ⇄ suspended
     │              │           │        │
     └──────────────┴───────────┴────────┴──► retired

Each state means something operationally:

  • registered — it exists in the inventory; it cannot obtain credentials.
  • approved — a human has reviewed it (scopes, owner, purpose).
  • active — it may obtain credentials.
  • suspended — reversible stop. Its next credential request fails.
  • retired — terminal.

Two design points that matter more than they look:

Every NHI has a named human owner. This is the single most common finding in an identity audit: credentials that exist, work, and belong to nobody, because the team that created them was reorganized. The lab makes owner mandatory at registration.

Revocation latency is the credential TTL. Suspension stops the next issuance; an already-issued credential keeps working until it expires. That is the honest statement, and it is precisely why 60-second lifetimes matter — they turn "revocation" from a distributed-systems problem into a wait.

If you need faster, you need a second channel: a kill switch that the resource server consults, which reintroduces the availability question from Phase 00. Most platforms conclude that short TTLs plus a kill switch for the small set of high-impact identities is the right shape.

11. Just-in-time credentials

Putting it together: a credential minted at the moment of use, for one audience, with one task's scopes, expiring in seconds, never stored.

broker.issue(CredentialRequest(
    identity_id="payments-investigator",
    audience="core-banking",
    scopes=("payments.read",),
    subject_token=user_token,          # keeps the user in the chain
    bind_to_key="pk-investigator",     # sender-constrained
    lifetime_seconds=60,
))

Why each property earns its place:

PropertyRemoves
Not storedthe secret-in-config leak, the secret-in-image leak, the secret-in-logs leak
60-second lifemost of the value of any leak, and the need for a rotation process
One audienceits usefulness anywhere else
Task scopesexcessive agency
Key-boundtheft-is-sufficient

And the branch that carries the phase's argument: with a subject token, the broker exchanges; without one, it mints a workload credential. The workload credential can do only what the workload may do on its own behalf — so anything requiring a user's entitlement must supply the user's token, and the platform cannot accidentally act as itself.

12. Enterprise integration

None of this is built from scratch in a bank. What you integrate with:

Microsoft Entra ID is almost certainly the authorization server. Three features map directly:

  • Managed identity — an Azure resource gets an identity with no secret; the platform issues tokens to it. Azure's answer to §8.
  • Workload identity federation — an external workload (a Kubernetes service account, a GitHub Actions run) exchanges its own token for an Entra token, with no stored secret. This is how you remove credentials from CI/CD.
  • On-behalf-of (OBO) — Entra's flow for a middle-tier service calling a downstream API with the user's identity. It is RFC 8693's delegation case, and it is what you will actually configure.

PAM (privileged access management) brokers, records and time-bounds privileged access. Agents touching privileged systems must go through it, not around it — which usually means the agent requests a session and PAM issues the credential, keeping the recording and the time bound intact.

Secrets management (Key Vault, Vault) is for what remains after §11 — and the goal is that very little does. A useful metric: count the long-lived secrets in the platform and drive it toward zero.

The integration question that decides your design: can Entra issue tokens with the act claim you need, or do you need your own STS? Most banks end up with a thin internal STS that consumes Entra tokens and issues platform-scoped ones with the chain — which is exactly the lab's TokenExchange.

13. Lab walkthrough

Work Lab 01 in this order — later sections depend on earlier ones.

  1. b64url_encode / decode (§3.1). The padding restore is the whole trick.
  2. Claims.to_payload / from_payload, _nest_actors / _flatten_actors (§3.2, §7.2). Nesting is latest-outermost; the round trip is earliest-first.
  3. _json_b64, Signer (§3.3). Canonical JSON; algorithm from the signer; compare_digest.
  4. Verifier.verify (§3.2, §3.4). Ten checks in the documented order. Signature first.
  5. Scopes (§6). narrow_scopes must be structurally incapable of widening.
  6. code_challenge, AuthorizationServer (§4). Mark a code used before the expiry check.
  7. TokenExchange.exchange (§7.3). Verify, then the four rules, then mint.
  8. SPIFFE/SPIRE (§8). All selectors must match; ambiguity is an error.
  9. IdentityRegistry (§10). A declared transition table; a mandatory owner.
  10. JitCredentialBroker (§11). The subject-token branch is the lesson.

Then python solution.py and read the six sections against §§3–11.

14. Success criteria

Without the guide open:

  • Explain why a service account fails for agents, with the audit record it produces.
  • Give the four properties of an agent credential.
  • Name the claims a resource server must check and the attack each prevents.
  • Explain both classic JWT breaks and their single shared fix.
  • Explain the confused deputy concretely, in an agent platform.
  • Derive PKCE and explain why plain is useless.
  • List what OAuth 2.1 removed and why.
  • Explain refresh-token rotation and what a reuse implies.
  • Distinguish an ID token from an access token by audience.
  • Explain why an agent gets the task's scope, not the user's.
  • State the four rules of a token exchange.
  • Explain delegation vs impersonation and which a bank wants.
  • Explain SPIFFE's inversion of trust, and what "secret-less" means.
  • Explain why all selectors must match and why ambiguity is refused.
  • Explain sender-constraint and when it is worth the complexity.
  • State the NHI lifecycle and why every one needs a human owner.
  • Explain why revocation latency equals the credential TTL.

15. Common mistakes

A service account for the agent fleet. Every action attributed to the platform.

Forwarding the incoming token to the next hop. The confused deputy.

Not checking aud. The single most commonly missing check.

Reading alg from the token. alg: none and algorithm confusion.

== on a signature. Leaks it byte by byte.

Forgetting base64url padding. Fails on two thirds of tokens.

Prefix redirect-URI matching. Open redirect → code interception.

plain PKCE. Protects against nothing.

Sending an ID token to an API. Audience is the client.

Giving an agent the user's full scope. Excessive agency by default.

Silently granting less than requested. The caller fails far from the cause.

An exchange that does not narrow. It has achieved nothing.

A derived token that outlives its parent. Escalation in the time dimension.

No cycle check on the chain. A→B→A across two owners, invisible to both.

Impersonation because it is simpler. The user disappears from the audit record.

Subset selector matching in attestation. Any workload in the namespace claims the identity.

Guessing when two registrations match. Nondeterministic identity.

Implicit cross-domain trust. Federation is always explicit.

An NHI with no human owner. The standard audit finding.

Assuming suspension is instant. It takes effect at the next issuance.

16. Interview Q&A

Q: Design the identity model for our agent platform.

A: "Four properties, and each is a control I can demonstrate: derived, narrowed, chained, short-lived. The user authenticates to Entra through the channel with authorization-code plus PKCE, and gets an access token audienced to the platform. Every hop after that is an RFC 8693 token exchange, not a forward — forwarding is the confused deputy, and a service account erases the user. Each exchange narrows the audience to the next service, subsets the scope to what this task needs rather than what the user may do, appends the actor to the act chain, and takes the minimum of the requested lifetime, policy, and the parent's remaining life — because a derived credential outliving its parent is escalation in the time dimension. Underneath, workloads get SPIFFE identities from attested properties rather than secrets, and credentials are bound to a key via cnf so theft alone isn't sufficient. The result is that core banking sees 'user u-42 asked, orchestrator delegated, investigator is acting, read-only, expires in 90 seconds' — which is a sentence I can put in front of an examiner."

Q: Why not a service account?

A: "Because of the audit record it produces. It says actor=svc-ai-platform and nothing else — you can't answer who asked, you can't bound the action by what that person is personally entitled to do, and a leak gives an attacker every permission every agent has, permanently. Then the structural problems: an agent acts on behalf of a person, so its authority should be derived and bounded; it acts across hops, so authority must narrow at each one; it discovers tools at runtime, so you can't enumerate permissions at design time; and there are hundreds of them, so one credential means either over-privileging everything or maintaining hundreds of accounts by hand. Every one of those breaks the model. Client credentials is right for a genuinely user-less workload — a nightly batch — and wrong the moment a person is involved."

Q: Walk me through what happens to the token across three hops.

A: "Hop zero: the user's token is audienced to the agent platform, scoped to what the channel requested, with no actor chain. Hop one: the orchestrator exchanges it — verifies it first, then mints a new token audienced to the investigator agent, with the same or narrower scope, sub still the user, and act containing the orchestrator. Hop two: the investigator exchanges again — audience becomes core-banking, scope narrows to payments.read because reading is all this task needs, and the chain becomes user → orchestrator → investigator. Each token's lifetime is the minimum of what was asked for, what policy allows, and what remains of the parent's life. And the chain at each hop is derived from a verified token, never from anything in the request — a callee that could assert its own position could erase a hop, and it would be the interesting one. Refusals along the way: an exchange that doesn't narrow the audience, one that widens scope, one that would form a cycle, and one from a token marked non-delegable."

Q: What's the confused deputy, in this system?

A: "A privileged intermediary tricked into using its authority for someone else. Concretely: the investigator agent holds a token a user presented to it, and forwards that same token to core banking. If core banking doesn't validate aud, it accepts a credential that was never intended for it — and the agent has used the user's authority against a service the user never chose. The defence is audience validation at every resource server, full stop, regardless of how valid the signature is. And the reason token exchange exists is that forwarding is the obvious implementation — you have a valid token, the next service needs one, so you pass it along. Every platform builds that first."

Q: Explain PKCE, and why OAuth 2.1 made it mandatory for everyone.

A: "The attack is code interception: a public client has no secret, so anyone who intercepts the redirect — a malicious app registering the same URI scheme — can redeem the code with just the public client id. PKCE fixes it by having the client generate a random verifier, send its SHA-256 hash as the challenge at the authorize step, and present the verifier at the token step. An attacker with the code doesn't have the verifier and can't derive it from the hash. plain mode, where the challenge is the verifier, protects against nothing, which is why 2.1 removes it. And it's mandatory for confidential clients too because a client secret defends against a different attacker — one who can call the token endpoint but can't see the redirect — so the two are orthogonal rather than alternatives."

Q: What does SPIFFE change?

A: "It inverts the direction of trust. Normally a workload presents a secret to prove what it is, which means the secret must be provisioned, rotated, protected, and can be copied. SPIFFE says the workload presents nothing: the platform attests properties it can independently verify — this node via an instance-identity document or TPM, this pod's namespace, service account and image — and issues a short-lived SVID based on them. There's no durable secret to steal, and an exfiltrated SVID is useless to an attacker who can't reproduce the selectors. Two implementation rules matter: all registered selectors must match, because a subset match lets any workload in the namespace claim a narrower identity; and if two registrations match, refuse rather than guess, because nondeterministic identity is the worst property an identity system can have. And it composes nicely with sender-constrained tokens — once workloads have certificates for mTLS, binding tokens to them is nearly free."

Q: An agent is compromised. How fast can you stop it?

A: "Suspension takes effect at the next credential request, so worst case is the credential TTL — which is exactly why I'd run 60-second lifetimes rather than hourly ones. That turns revocation from a distributed cache-invalidation problem into a wait. If a minute is too long for a specific identity, you need a second channel: a kill switch the resource server consults, which reintroduces a synchronous dependency and the fail-open/fail-shut question from the platform's availability model. Most platforms land on short TTLs everywhere plus a kill switch for the small set of high-impact identities. What I'd also want is the blast radius already bounded: because each credential is audienced to one service and scoped to one task, a compromised agent can't pivot — it has a 60-second read-only token for one API, not a service account with the union of everything."

17. References

Core specifications

  • OAuth 2.1 (draft) — the consolidation; read the "differences from 2.0" section first.
  • RFC 6749 (OAuth 2.0) and RFC 9700 (OAuth 2.0 Security Best Current Practice) — the BCP is where most of 2.1's changes come from and it explains the attacks.
  • RFC 7636 — PKCE.
  • RFC 8693 — OAuth 2.0 Token Exchange. §2 (the request) and §4.1 (the act claim) are the parts this phase is built on.
  • RFC 7519 (JWT), RFC 9068 (JWT profile for access tokens), RFC 7515 (JWS), RFC 7638 (JWK thumbprint), RFC 7800 (cnf).
  • RFC 8705 (mTLS client authentication and certificate-bound tokens) and RFC 9449 (DPoP).
  • OpenID Connect Core 1.0 — the ID token, and §3.1.3.7 on ID-token validation.

Workload identity

  • SPIFFE — the SPIFFE ID and SVID specifications, and the concepts pages on trust domains and federation.
  • SPIRE documentation — node and workload attestation, registration entries, the Workload API.

Enterprise

  • Microsoft Entra ID — on-behalf-of flow, managed identities, workload identity federation. These three are what you will actually configure.
  • HashiCorp Vault — dynamic secrets and identity brokering, as the JIT-credential pattern in production.

Background

  • Hardt's OAuth talks and the IETF OAuth working-group drafts, for why the protocol looks like it does.
  • OWASP — Top 10 for LLM Applications, Excessive Agency, which is what scope narrowing exists to close.
  • Any NHI-security industry report for the human-to-non-human identity ratio; the number moves, and the direction does not.