A user signs into the travel app, which calls the expense app, which must call the corporate card ledger. The first hop is easy: the travel app holds the user’s ID Token and exchanges it for an ID-JAG addressed to the expense app’s Resource Authorization Server. ID-JAG is built for exactly that: present a user credential, get an audience-scoped grant.
The expense app has none. It received a service call, not a browser session: no ID Token, no SAML assertion, no refresh token for the user. The expense app inherited the work but not the user. Yet to reach the ledger it needs an ID-JAG carrying the ledger’s name for the user, and only the IdP can produce that.
That last point is the argument of Re-Subjecting Is a Mint, Not an Attenuation: when a chain crosses into an application that names the user differently, only the IdP can mint the next hop, because only the IdP holds the subject map. But the IdP can only mint again if something it trusts survives the previous hop to attest that the chain is still live. That is the invariant everything here follows from, and it is exactly what the intermediate is missing. ID-JAG says what to present at the first hop and goes quiet about the rest. That silence is the gap, and closing it is this whole post.
This is no longer a human-paced problem. An agent acting for the user triggers these chains at machine speed and fans out to services nobody listed when the task began, with no one to send back through a login screen when a hop lacks a credential. The design this post walks through is published as draft-mcguinness-oauth-id-continuation-assertion, an individual Internet-Draft at revision 00.
What the Intermediate Is Allowed to Present
Start by ruling out the obvious answers, because each fails for a precise reason, and the reasons add up to a specification.
Forward the user’s ID Token. The intermediate never had a browser session, so there is no ID Token to forward. Pass one along anyway and it is still audience-bound to the original app and names the user in the original namespace, not the ledger’s. It also scatters a bearer user credential across every service in the chain, which is the sprawl federation was meant to end.
Reuse the inbound ID-JAG. The expense app holds an ID-JAG for the expense Resource Authorization Server, carrying the expense app’s pairwise subject. Sent to the ledger it is wrong three ways: wrong audience, wrong subject namespace, and it is an access-bearing grant, not a request for a new one.
Use a refresh token. It can mint new tokens, but it is a long-lived bearer credential bound to the original client and audience, carrying none of the chain or actor context. ID-JAG accepts one at the first hop because the holder actually authenticated the user. The intermediate did not.
Pre-mint every audience at the root. That works only for a closed, known set of targets, and it becomes a standing bundle of bearer authority that removes the per-hop decision. Dynamic fan-out breaks it the moment the chain reaches an audience nobody listed.
The failures converge. What the intermediate may present has to be something it can hold without a user session, that names no user and carries no access, that is bound to the intermediate and cannot be replayed, and that the IdP can verify ties to a real, still-live delegation. In a phrase: evidence, not authority. That is the Identity Continuation Assertion.
Evidence, Not a Credential
It is a short-lived, sender-constrained JWT, issued by a Chain Authority the IdP trusts, presented as the Token Exchange subject token to request an onward ID-JAG. It is not an ID-JAG, and it grants no API access.
Decoded, the assertion for the expense app’s onward call looks like this:
| |
| Claim | Purpose |
|---|---|
iss | The Chain Authority that issued the assertion, trusted by the IdP for the tenant |
aud | The Continuation Authorization Server (the IdP), a single issuer identifier |
identity_continuation_handle | Opaque, IdP-generated handle for this hop of the chain, at least 128 bits of entropy and no user-identifying information |
act | The single current actor presenting the request, as iss and sub per RFC 8693 with no nesting. The IdP composes the full lineage into the onward ID-JAG |
cnf | Sender-constraint by DPoP to the key the presenting actor proves, carried as its JWK thumbprint (jkt) |
iat / exp | Issuance and expiry, lifetime no more than 300 seconds |
jti | Single-use replay identifier, atomically consumed by the IdP on issuance |
It is presented in a Token Exchange request that is identical in shape to a direct ID-JAG request, with only the subject token swapped:
| |
The audience, resource, scope, and requested token type stay in the request, never in the assertion, so a direct request and a continued one have the same shape. What the assertion refuses to carry is the point. It MUST NOT contain a top-level sub, auth_time, acr, or amr. It does not name the user, and it does not claim how or when the user authenticated. Those are facts the IdP already holds. The assertion only attests that the chain is live. You cannot inflate a subject or an assurance level you were never given.
What it adds is deliberately narrow. For the applications in the chain the surface barely moves: the same Token Exchange, one new subject-token type, and the same ID-JAG out the other side. The new weight sits in the control plane, where the IdP already holds the subject map and the delegation state, plus one role that has to exist, a Chain Authority that can see the live chain. The assertion only gives a credential-less intermediate a sanctioned way to ask. No new authority is minted that ID-JAG did not already mint, and no Resource Server learns anything it did not already know. It changes who may ask, not what authority exists.
So Why Would the IdP Trust It?
Stop on that, because it should make a security reviewer nervous. A service that never saw the user, holding no user credential, just asked the IdP to mint a grant for that user at a system it has never talked to. Honor that on the service’s say-so and you have built the confused deputy on purpose.
The assertion is safe to trust only because four things are true at once, and each is a way of saying the same thing: it is evidence, not authority.
It does not come from the service. The issuer is a Chain Authority, and the IdP honors only the ones its tenant configuration names. Ask why this role has to exist at all. The IdP cannot vouch for the chain itself, because the work left its hands at the first hop and now runs between services it never sees. The intermediate cannot vouch for itself, because a service asserting its own continuation is the confused deputy this design exists to prevent. What is left is a party that can actually see the in-flight delegation and that the IdP has agreed in advance to believe, usually the transaction-context service that propagates user and request context across a domain’s internal calls, signing an attestation that this chain is live. The IdP is trusting a configured authority’s signed statement, not the asker’s word, and an intermediate cannot appoint itself one.
It names no one. With no subject and no authentication context in the assertion, there is nothing for the intermediate to spoof. The IdP resolves the ledger’s subject from identity_continuation_handle against its own record, and it trusts its own memory of how the user authenticated.
It is bound and spent once. It MUST be sender-constrained through cnf to the key the presenting actor proves, by DPoP, and MUST NOT be accepted as a bearer token. The actor token presented alongside it MUST be bound to that same key, so the whole request rests on one proof of possession. Its lifetime MUST be no more than 300 seconds. The IdP consumes the (iss, jti) pair atomically on success and honors it at most once. Copy the assertion off the wire and you hold a 300-second, single-use token bound to a key you do not have: the IdP wants a proof of possession you cannot produce, and if the real caller already spent it, the jti is gone. A stolen assertion buys nothing.
It is an input, not a verdict. Even with all of that, the IdP decides. It confirms identity_continuation_handle is active and eligible, that the requested audience and scope sit inside the root delegation’s envelope, and that the actor named in act is the authenticated caller, and only then mints. The assertion buys a fresh decision, not access.
That is the resolution. An intermediate can drive the chain forward without ever being trusted as an authority, because the only authority in the exchange is the IdP, deciding again, against state it owns.
What It Looks Like Running
Strip it to the moving parts, split into a control plane the IdP owns and a data plane the apps see.
The control plane is the enterprise IdP, acting as the Continuation Authorization Server. It owns the subject map and exposes the token-exchange endpoint that mints ID-JAGs. Behind it sit three pieces of state. A root-chain envelope records the authenticated user, the original authentication context, the authorization basis as enumerated target entries or a policy reference, the governing authorization whose lifetime bounds how long the chain may continue, which actors or trust domains may continue it, the maximum actor-chain depth, an expiry, and the authenticated root actor, with each hop recorded under its own identity_continuation_handle and an immutable reference to its parent, so the chain is a tree the IdP walks rather than one growing token. A single-use store records each consumed (iss, jti) until the assertion expires. A subject resolver turns the canonical user into the audience-local pairwise subject for a given target, by directory lookup or deterministic per-sector derivation. Alongside the IdP, each domain runs a Chain Authority, often a transaction-context sidecar, that holds the live chain context and signs assertions with a key the IdP has registered. The IdP records, for each hop, which Chain Authorities may attest it, so only one mapped to that hop’s Resource Authorization Server can continue it, and the trust is hop-specific rather than tenant-wide.
The data plane is the workloads and the Resource Authorization Servers. Each workload holds a sender-constraining key. Each Resource Authorization Server trusts the IdP, accepts an ID-JAG as a JWT authorization grant, binds the hop handle it carries to the authorization state it grants, mints its own access token, and knows the user only under its own pairwise subject. The subject map lives only in the control plane and never leaves it. The hop handle does travel, as a claim in the ID-JAG, so a workload carrying it is a control-plane participant for that hop, but it stops at the Resource Authorization Server that redeems the grant. The Resource Server behind it is issued an ordinary access token and never sees the handle.
Traced end to end, the two hops look like this, with the IdP consulted at each boundary:
Hop one establishes the chain. The travel app exchanges the user’s ID Token at the IdP for an ID-JAG addressed to the expense Resource Authorization Server, in a request that looks exactly like a direct exchange. Where tenant policy allows the delegation to be continued, the IdP establishes a chain on its own, opening a root-chain envelope for it and embedding a fresh identity_continuation_handle as a claim in the ID-JAG it issues, for the root hop and for every hop after. There is no separate response parameter. The presence of the handle claim is the signal that the delegation is continuation-capable, and its absence that it is not. The travel app presents the ID-JAG, whose sub is the expense app’s alone, to the expense Resource Authorization Server, which binds the handle to the authorization state it grants and mints an access token. The travel app calls the expense API with that token. Within the expense domain the handle lives on as chain context the Chain Authority sidecar holds, ready for the next hop, and never in the access token the API sees.
Hop two hits the gap. The expense service must post a charge to the ledger and holds nothing the ledger will accept. Its Chain Authority sidecar signs an Identity Continuation Assertion for that identity_continuation_handle, and the expense service presents it to the token-exchange endpoint with audience the ledger Resource Authorization Server, scope=charges.write, its workload credential as the actor token, and a proof of possession of its key.
The IdP runs the gate against the control plane. It verifies the signature against the registered Chain Authority key, using an allowed asymmetric algorithm and never none, and checks the oauth-identity-continuation+jwt type, confirms it is the audience and the issuer is a configured Chain Authority, and looks the identity_continuation_handle up in the root-chain envelope, which must be present and active, with the ledger a permitted target, the expense service permitted to continue the chain from this hop, charges.write inside the authorization basis, and the chain still within its maximum actor-chain depth. The proof-of-possession key must match cnf, the actor token must be bound to that same key and authenticate the current actor, and the caller must authenticate as an OAuth client that is that same actor. The IdP atomically consumes (iss, jti). If it was already spent, the request is refused. Only then does the subject resolver map the canonical user to the ledger’s pairwise subject, led-c7e0, and the IdP mints an ID-JAG with itself as issuer, the ledger as audience, that subject, the full actor chain it composes from the actors it authenticated at each hop since the assertion named only the current one, and a fresh identity_continuation_handle claim for the new hop. The expense service presents the ID-JAG to the ledger Resource Authorization Server as a JWT authorization grant (RFC 7523), which binds the new handle, applies its own policy, and issues an access token. The charge posts, for the user, under the ledger’s name for the user, with the acting workload on the record.
The Round-Trip Is the Point
Asking the IdP at every boundary looks like the cost of this design. It is the feature.
One continuation is the unit, and a chain is the same unit repeated. When the ledger needs a fourth service, its Chain Authority issues a fresh assertion referencing the handle for the ledger’s hop, the IdP records a new hop under a fresh identity_continuation_handle with an immutable reference to its parent, re-resolves the next audience’s subject, and decides again. The actor lineage the IdP composes grows by one, up to the maximum depth the envelope records. Handles are hop-specific by construction, so no two hops, and no two audiences, ever share one, and the map from any hop back to the root delegation lives only in the control plane. Each onward ID-JAG carries a handle for its own hop, but that handle stops at the authorization server that redeems it, so a Resource Server, issued only an ordinary access token, has nothing to link the user by across SaaS boundaries. Colluding audiences are the harder case. They share no handle to join on, but the actor lineage each onward ID-JAG carries, together with timing, remains a correlation channel the handle design does not close.
Because the IdP is asked at every hop, each continuation is a fresh policy point. A revoked delegation, the work behind the chain ending, a risk signal that fired since the chain began, all stop the chain at the next boundary instead of being honored from a stale token. The chain’s continuation lifetime is bounded by the governing authorization behind it, a real grant or session rather than a token, so a session-anchored chain cannot outlive its session and only a grant-anchored one survives logout. Ending that authorization ends the chain, and the IdP can also cut a single branch, revoking one hop’s subtree while the rest keeps going. Revocation is a single fact in one place: the control plane marks the chain inactive, and the next continuation resolves its handle against that state and finds it gone. What it stops is the next hop, not the last one, so an ID-JAG or access token already minted lives out its own short lifetime, and the honest promise is bounded: a chain that cannot be extended past the authority that justified it, closing over the short life of the grants in flight rather than instantly. That is still what scattered long-lived tokens never gave you, one record the IdP owns and re-checks every time identity crosses a boundary, instead of authority hunted across three SaaS providers with nothing to re-check against.
What It Is, and What It Is Not
The boundaries keep its job clear. It is presented to the IdP to obtain an ID-JAG and authorizes nothing on its own. It is not a bearer token: possession is not enough, the presenter must prove the confirmed key. It does not let the holder compute or assert the target subject. The IdP does that. And it reuses ID-JAG unchanged as the onward grant rather than defining a competitor. It is a continuation extension of ID-JAG, not a replacement.
Its scope is narrow on purpose: same-IdP SaaS-to-SaaS, where several Resource Authorization Servers trust one enterprise IdP, and only for hops that re-subject. If the next audience accepts the same subject, no mint is needed and offline attenuation is the right tool. Transaction Tokens are a neighbor at a different layer, propagating request context within a call chain rather than minting the onward audience-scoped grant this produces. The case this does not address is a single global subject, the SPIFFE-style workload trust domain, where every service already names the user the same way and there is nothing to re-subject.
That places it cleanly among the grants it works with, all under OAuth Identity and Authorization Chaining. ID-JAG covers the first hop, where an application holds the user’s credential to exchange. The Identity Continuation Assertion covers the hops after that, where the application holds no credential but the chain is still live. Both produce the same onward artifact, an audience-scoped ID-JAG. In the frame of Continuity Is Not One Thing, this is identity continuity at the credential-less hop, and every continuation is a decide-anew transition: evidence crosses, the IdP decides again, and authorization is never inferred from identity.
Get Involved
The Identity Continuation Assertion is published as an individual Internet-Draft at revision 00, with the editor’s copy tracking between revisions. The review that matters comes from the people who would run it: identity providers that would mint the onward grants and resolve each audience’s subject, SaaS platforms whose services sit in the middle of these chains, and agent runtimes that fan out across applications with no human at each hop. The questions I most want pressure on: is the Chain Authority the right trust boundary, is the per-hop round-trip acceptable at machine speed, and does the identity_continuation_handle confinement hold up against colluding audiences. If you operate one of these systems, tell me where it breaks.