Claim anonymous registration via ID-JAG#12
Conversation
ba28afd to
81dfe27
Compare
808fd40 to
4926a3c
Compare
|
@greptileai review |
Greptile SummaryThis PR adds a second body shape to
Confidence Score: 5/5Safe to merge — the core claim path is correctly guarded and the previous review's concerns are addressed. The email-immutability guard, agent-services/src/routes/agent-auth.ts — specifically the ordering of Important Files Changed
|
81dfe27 to
ff431df
Compare
54deca0 to
8cae8f2
Compare
ff431df to
2870085
Compare
8cae8f2 to
e624e5f
Compare
acd4727 to
8d3c948
Compare
e624e5f to
28ac6e1
Compare
8d3c948 to
37cc113
Compare
090e1b6 to
617a4fb
Compare
46b24a3 to
68784d9
Compare
554ae3a to
06058af
Compare
68784d9 to
3bb0904
Compare
|
@greptile |
1df9a5a to
47ac80d
Compare
583542a to
35ae83b
Compare
|
@greptile |
1 similar comment
|
@greptile |
deb9929 to
bfaa624
Compare
bfaa624 to
b6b9cc7
Compare
b6b9cc7 to
e238dad
Compare
Same treatment as the PR #11 fix for the step-up response: name the actual JSON key rather than introducing a separate "ceremony block" term. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@greptile |
Adds a second shape to POST /agent/identity/claim, discriminated on `type`:
{ type: "email", claim_token, email } → start user_code ceremony (existing)
{ type: "identity_assertion", claim_token, assertion } → claim atomically via ID-JAG (new)
The agent flow this unlocks: start anonymous, do read-only work pre-claim,
later acquire an ID-JAG (user signed in at the provider mid-run) and bind
the existing anonymous registration to that user — keeping the
registration_id and pre-claim continuity intact instead of throwing it
away and re-registering.
Service code
- schemas.ts: claimBody becomes a discriminated union on `type`. The
email-shape variant is what existed; the identity_assertion shape is new.
- store.ts: completeAnonymousClaimViaIdJag binds an anonymous registration
to a verified ID-JAG. Sets user_id + claimed_at, records id_jag triple,
upsertDelegation, revokes pre-claim access_tokens. Anonymous-only — the
email-verification path doesn't accept this shape.
- agent-auth.ts /claim handler dispatches on parsed.value.type. The new
handleAnonymousClaimViaIdJag runs the same verifyIdJag + matcher chain
as /agent/identity, mints a v2 identity_assertion on success.
- Step-up case: if the matcher would return step_up_required (ID-JAG's
email matches a different existing user, no delegation yet), refuse with
401 interaction_required pointing the agent to /agent/identity to walk
normal step-up first; the binding can complete on retry.
- auth_time errors flow through handleIdJagVerifyError so they 401 as
login_required, same as /agent/identity.
Demo + docs
- home.ts: existing anon claim call now sends the type field.
- AUTH.md Step 4 documents both shapes; new "4a-alt. Claim via ID-JAG"
subsection.
- agent-services README documents the two shapes + the new path.
Smoke test
- Anonymous register → pre-claim jwt-bearer → API call (api.read) → 200 ✓
- POST /claim with type=identity_assertion → 200, v2 assertion has email ✓
- Pre-claim access_token → 401 (revoked) ✓
- v2 → jwt-bearer → full scopes → API call 200 ✓
- Fresh ID-JAG to /agent/identity clean-matches (delegation persisted) ✓
- email-shape claim still works (no regression) ✓
- AUTH.md, agent-services/README.md: the step-up branch of POST /agent/identity/claim (type: identity_assertion) returns 200 with a ceremony block, not 401 interaction_required as the docs claimed. Rewrote the section to show both terminal shapes (clean-match 200 + identity_assertion, step-up 200 + claim_attempt) accurately. - agent-auth.ts: move the email-immutability check inside the email- type branch so parsed.value.email narrows correctly; the prior position type-erred against the new discriminated union. Rename the stale recordAnonymousClaimAttempt call to recordClaimAttempt (carried over from PR #10's rename). Update the handleAnonymousClaimViaIdJag docstring to describe both terminal shapes instead of the old "refuse step-up" wording. - home.ts: claim preview body sent type: "user_code", which the discriminated union rejects. Change to type: "email". - CHANGELOG.md: v0.6.0 entry for the new discriminated union body shape and its two terminal responses. - package.json: 0.5.0 → 0.6.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The id_jag-kind guard at the top of /claim went away upstream so step-up id_jag registrations can refresh their user_code via the email-shape body. That removal opened a separate door: an agent could now send type: "identity_assertion" against an email_verification or id_jag step-up registration and enter handleAnonymousClaimViaIdJag, which assumes anonymous semantics throughout (atomic bind, no existing user to reconcile against). Add an explicit kind === "anonymous" gate inside the type: "identity_assertion" branch with a message pointing the agent at the email-shape body for non-anonymous refresh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both terminal responses from handleAnonymousClaimViaIdJag (clean-match status: "claimed" and step-up status: "initiated") were missing the registration_type field that every other /agent/identity response includes. Add "identity_assertion" to both so the shape is consistent with the corresponding /agent/identity responses for the same flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the narrow "user signed in mid-run" framing with one that
covers the actual trigger ("user wants to claim AND agent can obtain
an ID-JAG"), and swap implementation jargon ("clean match",
"step-up") for what the agent sees ("no confirmation needed",
"confirmation required"). Also restore the v0.6.0 CHANGELOG wording
to "can be used without further verification" / "confirmation is
required".
agent-services/README.md keeps step-up / clean-match in the
implementer-facing sections (the reader is the one writing the
matcher); only the "Claim via ID-JAG" opener gets the broader Pattern 1
rewording.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same treatment as the PR #11 fix for the step-up response: name the actual JSON key rather than introducing a separate "ceremony block" term. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ocks Two correctness gaps in the anonymous-claim-via-ID-JAG flow: - handleAnonymousClaimViaIdJag ran the matcher and proceeded straight into recordClaimAttempt (step-up) or completeAnonymousClaimViaIdJag (clean match) without checking whether the registration already had a bound email from a prior /claim call. That let an agent override registration.claim.email by swapping in an ID-JAG resolving to a different user — the same hijack the email-shape path's email-immutability guard prevents. - completeAnonymousClaimViaIdJag rejected claimed and expired registrations but not pending_claim, so an atomic ID-JAG claim could silently complete a registration with an in-flight ceremony. Add a route-level email-immutability check that fires for both the step-up and clean-match branches: if registration.claim.email is set, the user the ID-JAG resolves to must have that email. Returns 400 email_mismatch. Add a store-layer pending_claim guard as defense-in-depth — even if a code path reaches the store function with an in-flight ceremony, it now rejects with ceremony_in_flight (mapped to 409 at the route). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59d363e to
8ce2b1d
Compare
Aligning with the actual merge date. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@greptile |
Stack
Summary
POST /agent/identity/claimgains a second body shape, discriminated ontype:The flow this unlocks: an agent starts anonymous, does pre-claim work, later acquires an ID-JAG (user signed in at the agent's provider mid-run) and binds the existing anonymous registration to that user. Keeps
registration_idand pre-claim continuity intact instead of throwing it away and re-registering.Verification chain
/claimwithtype: identity_assertionruns the same verification chain as/agent/identity. Failures route the same way:auth_timemissing or stalelogin_requiredwithWWW-Authenticate: AgentAuth error="login_required", max_age="…"(iss, sub)delegation OR JIT (no email conflict)identity_assertion— ID-JAG alone is enough; bound atomicallystep_up_required(ID-JAG's email matches an existing different user, no delegation yet)claim_attemptceremony block — user must confirm at/claim. The ceremony binds the anonymous registration AND the(iss, sub)delegation in one shot.The step-up branch returns the ceremony block right here rather than redirecting to
/agent/identity—/claimis already the user-mediated confirmation surface; there's no reason to bounce the agent across endpoints.Implementation
schemas.ts:claimBodybecomes az.discriminatedUnion("type", …).store.ts:recordAnonymousClaimAttemptaccepts an optionalidJagtriple. When set, the anonymous registration recordsid_jag = { iss, sub, aud }socompleteClaimupserts the delegation alongside the user binding.completeAnonymousClaimViaIdJaghandles the atomic clean-match path (no ceremony): binds user + delegation, recordsid_jag, revokes pre-claim access_tokens.completeClaimupserts delegation based onregistration.id_jagpresence (not kind), so both step-up-via-/identityand step-up-via-/claimpaths bind delegations on completion.agent-auth.ts/claimhandler dispatches onparsed.value.type. The newhandleAnonymousClaimViaIdJagrunsverifyIdJag→matchOrProvision→ branches onmatch.kind.typefield.Smoke test
Step-up path (ceremony required):
alice@example.comat the service via email-verifPOST /claimwithtype: identity_assertion+ ID-JAG → 200 withclaim_attempt(user_code 428947) ✓/claim→ 200/oauth2/tokenwith claim grant → scopeapi.read api.write, v2 assertion'semail: alice@example.com✓Clean-match path (ID-JAG enough):
6. Fresh anonymous register
7.
POST /claimwithtype: identity_assertion+ ID-JAG (same(iss, sub), delegation now exists from step 5) → 200 + immediate v2identity_assertion, no ceremony ✓