« Phase 08 · Warmup · Track Overview

Lab 01 — The Identity Fabric

The problem

A relationship manager asks a question in Teams. An orchestrator agent takes it, delegates to a payments-investigation agent, which calls core banking to read a payment.

Three hops. At the last one, core banking must be able to answer: who is doing this, on whose behalf, with what authority, and can I prove it?

The tempting answer is a service account with the union of every permission any agent might need. It works on day one and it is the finding an examiner writes up: every action is attributed to "the platform", nothing is bounded by what the user may do, and a leaked credential is permanent and unlimited.

You build the alternative: credentials that are derived from a verified assertion, narrowed at every hop, chained so the whole path is visible, and short-lived enough that revocation is a timeout rather than a process.

What you build

#ComponentWhat it does
1b64url_*, Claims, SignerJWS with canonical JSON, and the claim set every check reads
2_nest_actors / _flatten_actorsRFC 8693's nested act chain, and its counter-intuitive ordering
3Verifier, ReplayCacheten checks in a deliberate order, each stopping a named attack
4narrow_scopes, require_no_escalationthe narrowing algebra — structurally incapable of widening
5code_challenge, AuthorizationServerOAuth 2.1: authorization code + mandatory PKCE, client credentials, OIDC ID tokens
6TokenExchangeRFC 8693 — narrow the audience, subset the scope, append the actor, shorten the life
7SpiffeID, SpireServer, mtls_authorizeworkload identity from attested selectors; mTLS with explicit federation
8IdentityRegistry, NHIStatethe non-human identity lifecycle, with a human owner
9JitCredentialBrokermint at the moment of use, bound to a key, expiring in seconds

Key concepts

ConceptWhereWhy it matters
Audience validationVerifier.verifythe confused-deputy defence — a token for us must not work elsewhere
alg from the signer, never the tokenverify_signature"alg: none" and algorithm confusion are the two classic JWT breaks
Constant-time comparehmac.compare_digest== leaks a signature byte by byte
Derived, not assertedTokenExchange.exchangethe chain comes from a verified subject token, never from the request
Narrowing is structuralnarrow_scopesthe function cannot return a scope it was not given
Refuse, don't silently droprequire_no_escalationa caller that thinks it has authority it lacks fails far from the cause
Chain cycleschain_cycleA→B→A is a loop across two owners with nobody able to see it
Never outlive the parentlifetime min(...)a derived credential with a longer life is a privilege escalation in time
Impersonation erases the chaindelegation=Falsewhich is exactly why a regulated platform disables it
Secret-less identitySpireServer.attestthe workload presents nothing; the platform observes and issues
All selectors must matchRegistrationEntry.matchesa subset match lets a workload claim a narrower identity
Ambiguity is refusedambiguous_attestationguessing assigns identity nondeterministically
Federation is explicitmtls_authorizecross-domain trust is never a default
Every NHI has a human ownerIdentityRegistry.registerthe standard identity-audit finding
Revocation latency = TTLJitCredentialBrokerwhy 60 seconds is a design decision, not a default

Files

FileRole
lab.pyyour implementation
solution.pyreference; python solution.py runs a six-part worked session
test_lab.py104 tests
requirements.txtpytest

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v
python solution.py

Success criteria

  • All 104 tests green against your lab.py.
  • Base64url round-trips for every padding case, and emits no =.
  • A token whose header says alg: none is refused with invalid_alg.
  • A token minted for another audience is refused — even with a valid signature.
  • Clock skew is tolerated in both directions, and has a limit.
  • act nests with the most recent actor outermost, and round-trips earliest-first.
  • narrow_scopes can never return a scope outside held, for any input.
  • PKCE is mandatory, S256 only, and a plain challenge raises.
  • An authorization code is single-use, exact-redirect-matched, and client-bound.
  • An exchange that does not narrow the audience is refused.
  • An agent already in the chain — or the user itself — cannot be appended.
  • A derived token never outlives its parent, even when a longer life is requested.
  • Attestation with a subset of the registered selectors fails.
  • Two matching registration entries produce ambiguous_attestation, not a guess.
  • A suspended identity cannot receive a credential.
  • A token bound to a key is useless without it.

How this maps to the real stack

This labThe real thingWhat we simplified
Signer (HS256)RS256/ES256 with a JWKS endpoint and key rotationsymmetric signing means the verifier could mint; real deployments must not allow that
Verifieryour API gateway, a resource-server library, or Entra's validation middlewareno JWKS fetching, caching or rollover
AuthorizationServerMicrosoft Entra ID, Okta, Auth0, Keycloakno consent, no refresh tokens, no discovery document, no DCR
TokenExchangeEntra's on-behalf-of flow; Keycloak and Auth0 token exchange; an in-house STSno requested_token_type, no actor-token parameter, no impersonation policy engine
SpireServerSPIRE, with node and workload attestors (k8s, AWS, GCP, Docker)no attestation plugins, no X.509 SVIDs, no Workload API socket, no trust-bundle rotation
mtls_authorizeIstio/Linkerd mTLS with SPIFFE identities, or Envoy RBACno TLS at all — the authorization logic is the point
IdentityRegistryan NHI governance product, or an internal serviceno discovery of unmanaged identities, no attestation of ownership
JitCredentialBrokerHashiCorp Vault dynamic secrets, cloud STS AssumeRole, Entra managed identityno secret engines, no lease renewal

Honest limits. No asymmetric signing, so nothing here exercises key distribution — which is most of the operational work. No refresh tokens, and therefore no refresh-token rotation or reuse-detection, which is where a lot of OAuth 2.1's remaining subtlety lives. No revocation list — the lab's revocation is expiry plus registry state, which is the right default and not the whole story. And the delegation chain is carried in a claim; a real deployment must also decide what happens when a hop crosses into a system that cannot read it.

Extensions

  1. Asymmetric signing and a JWKS endpoint. Then rotate the key with an overlap window and watch which of your tests break. Key rollover is where identity systems actually fail.
  2. Refresh tokens with rotation and reuse detection. A reused refresh token means it was stolen; the correct response is to revoke the whole family. Implement it and test the race.
  3. DPoP (RFC 9449). Replace the lab's cnf thumbprint with a real proof-of-possession: a signed JWT per request, with a nonce and replay protection.
  4. Continuous authorization. Re-verify mid-task (Phase 09): the user's entitlements change while a four-hour task is parked. What must interrupt?
  5. A revocation channel. Suspension currently takes effect at the next credential request. Add a kill switch that beats the TTL, and decide what it costs you in availability.
  6. Cross-domain federation. Two SPIRE trust domains with a federation bundle. What must be re-verified rather than trusted?
  7. The parked-task problem. A task waiting four hours for an approval outlives every sensible credential. Implement the correct shape: hold a reference, re-mint on resume, re-evaluate policy at that moment.

Interview / resume bullets

  • "Built the platform's agent identity model: every credential is derived from a verified assertion via RFC 8693 token exchange, narrowed in audience and scope at each hop, appended to an unforgeable delegation chain, and issued with a lifetime of seconds — so core banking can see that user U asked, orchestrator O delegated, and agent A acted."
  • "Replaced shared service accounts with SPIFFE-style workload identity: the platform attests verifiable properties of a running workload and issues a short-lived SVID, which removed long-lived secrets from the agent fleet entirely."
  • "Made privilege escalation structurally impossible in the exchange path — the narrowing function cannot return a scope it was not given — and made silent under-granting an error rather than a surprise."
  • "Implemented an NHI lifecycle with a mandatory human owner and a declared state machine, so 'which agents exist, who owns them, and are they still needed' became a query rather than an investigation."