---
title: "The Identity Continuation Assertion"
date: "2026-07-08T09:00:00-07:00"
lastmod: "2026-07-08T09:00:00-07:00"
description: "ID-JAG works at the first hop, where an application holds the user's credential to exchange. The hops after it hold no user credential, yet only the IdP can mint the next audience-scoped grant. The Identity Continuation Assertion is the missing input: short-lived, sender-constrained evidence of an in-flight delegation that an intermediate presents to the IdP to request an onward ID-JAG."
summary: "Re-subjecting across a SaaS boundary is a mint, not an attenuation, so the IdP must issue each onward grant. ID-JAG covers the first hop, where an application holds the user\u0026rsquo;s ID Token or SAML assertion to exchange. Subsequent hops hold no end-user credential, which leaves a gap: the intermediate has nothing to present to the IdP to continue the chain. The Identity Continuation Assertion fills it. It is a short-lived, sender-constrained JWT, issued by a Chain Authority the IdP trusts, that carries opaque evidence of the in-flight delegation and is presented as the Token Exchange subject token to request an onward ID-JAG. It is evidence, not authority: it carries no subject and grants no access, the IdP resolves the target audience\u0026rsquo;s subject and re-decides at every hop, and the chain correlator never reaches a Resource Server."
slug: "identity-continuation-assertion"
tags:
  - "Agentic Identity"
  - "ID-JAG"
  - "Identity Chaining"
  - "OAuth"
  - "Delegated Authority"
  - "IAM"
  - "XAA"
  - "Standards"
---


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](/notes/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. So the IdP can mint, and the intermediate has nothing to ask with. 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.

# 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:

```json
{
  "iss": "https://chain-authority.expense.example/",
  "aud": "https://idp.enterprise.example/",
  "chain_id": "01JZ8F4J9J8Y3NDK5WQ4P9K7Q2",
  "act": {
    "iss": "https://expense.example/",
    "sub": "svc-expense",
    "act": { "iss": "https://travel.example/", "sub": "svc-travel" }
  },
  "cnf": { "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I" },
  "iat": 1717700000,
  "exp": 1717700180,
  "jti": "ica-01JZ8GB2Q7"
}
```

| 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 |
| `chain_id` | Opaque, IdP-generated identifier for the root delegation, at least 128 bits, no user-identifying information |
| `act` | Actor lineage per RFC 8693, current actor outermost, identity claims only |
| `cnf` | Sender-constraint to the key the presenting actor proves, by DPoP or mutual TLS |
| `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:

```http
POST /token
Content-Type: application/x-www-form-urlencoded
DPoP: <proof signed by key with jkt 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I>

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:id-jag
&subject_token=<identity-continuation-assertion>
&subject_token_type=urn:ietf:params:oauth:token-type:identity-continuation
&audience=https%3A%2F%2Fledger.example%2F
&resource=https%3A%2F%2Fapi.ledger.example%2F
&scope=charges.write
&actor_token=<expense-workload-credential>
&actor_token_type=urn:ietf:params:oauth:token-type:jwt
```

The audience, resource, scope, and actor token 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.

Notice how little this adds. The same Token Exchange, one new subject-token type, and the same ID-JAG out the other side. The IdP already holds the subject map and the delegation state. The assertion only gives a credential-less intermediate a sanctioned way to ask. Nothing 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. A Chain Authority is the party that can actually see the in-flight delegation, usually the transaction-context service that propagates user and request context across a domain's internal calls, and it signs 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 `chain_id` 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 or mutual TLS, and MUST NOT be accepted as a bearer token. 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 180-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 `chain_id` is active and eligible, that the requested audience and scope sit inside the root delegation's envelope, and that the outermost actor 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**, indexed by `chain_id`, records the authenticated user, the original authentication context, the authorization basis as enumerated target entries or a policy reference, the maximum actor-chain depth, an expiry, and the authenticated root actor. 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 for the tenant.

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, mints its own access token, and knows the user only under its own pairwise subject. Nothing in the data plane ever holds `chain_id` or the subject map. Both live only in the control plane.

**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. Seeing a delegation eligible for continuation, the IdP opens a root-chain envelope under a fresh `chain_id` and returns that `chain_id`, along with an advisory `chain_exp` marking when the chain stops being continuable, as token-exchange response parameters, never as token claims. The ID-JAG it returns carries `sub = exp-4a17` and no `chain_id`. The travel app presents it to the expense Resource Authorization Server, gets an access token, and calls the expense API. The `chain_id` travels in the transaction context the sidecar manages, not in the access token.

**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 `chain_id`, 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 and the `oauth-identity-continuation+jwt` type, confirms it is the audience and the issuer is a configured Chain Authority, and looks the `chain_id` up in the root-chain envelope, which must be present and active, with the ledger a permitted target, `charges.write` inside the authorization basis, and the chain still within its maximum actor-chain depth. The proof-of-possession key must match `cnf`, and the actor token must authenticate the outermost 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 actor chain carried forward, and no `chain_id`. The expense service presents it to the ledger Resource Authorization Server as a JWT authorization grant (RFC 7523), which 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 for the same `chain_id`, the `act` lineage grows by one actor, up to the maximum depth the envelope records, and the IdP re-resolves the next audience's subject and decides again. The `chain_id` stays stable and stays in the control plane. Each onward ID-JAG carries only its own audience's subject, never the correlator, so a Resource Server that sees only its data-plane tokens cannot link the user across SaaS boundaries. Where even colluding participants must not correlate, the IdP can hand each audience a distinct chain reference that maps internally to one root.

Because the IdP is asked at every hop, each continuation is a fresh policy point. A revoked delegation, an ended mission, 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. Revocation is a single fact in one place: the control plane marks that `chain_id` inactive, the next continuation looks it up and finds it gone, and the chain halts. No token had to be hunted across three SaaS providers, because the authority that mattered never lived in those tokens. It lived in one record the IdP owns, and every hop went back and asked. A long-running agent chain cannot outlive the authority that justified it, because that authority is re-checked every time identity crosses a boundary.

# 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. The bilateral cross-domain case, where two domains translate subjects under an agreement, is the job of the [Transaction Token Authorization Grant Profile](https://datatracker.ietf.org/doc/draft-fletcher-transaction-token-chaining-profile/).

That places it cleanly among the grants it works with, all under [OAuth Identity and Authorization Chaining](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-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.

# Get Involved

The [Identity Continuation Assertion](https://mcguinness.github.io/draft-mcguinness-oauth-id-continuation-assertion/draft-mcguinness-oauth-id-continuation-assertion.html) is an early individual draft. 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 `chain_id` confinement hold up against colluding audiences. If you operate one of these systems, tell me where it breaks.

