Skip to content

Claim anonymous registration via ID-JAG#12

Open
m0tzy wants to merge 13 commits into
mainfrom
madison/claim-via-id-jag
Open

Claim anonymous registration via ID-JAG#12
m0tzy wants to merge 13 commits into
mainfrom
madison/claim-via-id-jag

Conversation

@m0tzy

@m0tzy m0tzy commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator
  • I have QA'd the changes

Stack

  1. Split credentials into a separate endpoint #8
  2. Migrate revocation channel to SET delivery (RFC 8417/8935) #9
  3. Invert the claim flow to mirror device auth #10
  4. ID-JAG step-up + auth_time freshness #11
  5. Claim anonymous registration via ID-JAG #12 ← you are here

Summary

POST /agent/identity/claim gains a second body shape, discriminated on type:

{ "type": "email", "claim_token": "...", "email": "..." }                      → start user_code ceremony (existing)
{ "type": "identity_assertion", "claim_token": "...", "assertion": "<ID-JAG>" } → claim atomically via ID-JAG (new)

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_id and pre-claim continuity intact instead of throwing it away and re-registering.

Verification chain

/claim with type: identity_assertion runs the same verification chain as /agent/identity. Failures route the same way:

ID-JAG state Response
Signature / audience / replay bad 400 with the verifier error code
auth_time missing or stale 401 login_required with WWW-Authenticate: AgentAuth error="login_required", max_age="…"
Matcher: existing (iss, sub) delegation OR JIT (no email conflict) 200 + v2 identity_assertion — ID-JAG alone is enough; bound atomically
Matcher: step_up_required (ID-JAG's email matches an existing different user, no delegation yet) 200 + claim_attempt ceremony 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/claim is already the user-mediated confirmation surface; there's no reason to bounce the agent across endpoints.

Implementation

  • schemas.ts: claimBody becomes a z.discriminatedUnion("type", …).
  • store.ts:
    • recordAnonymousClaimAttempt accepts an optional idJag triple. When set, the anonymous registration records id_jag = { iss, sub, aud } so completeClaim upserts the delegation alongside the user binding.
    • completeAnonymousClaimViaIdJag handles the atomic clean-match path (no ceremony): binds user + delegation, records id_jag, revokes pre-claim access_tokens.
    • completeClaim upserts delegation based on registration.id_jag presence (not kind), so both step-up-via-/identity and step-up-via-/claim paths bind delegations on completion.
  • agent-auth.ts /claim handler dispatches on parsed.value.type. The new handleAnonymousClaimViaIdJag runs verifyIdJagmatchOrProvision → branches on match.kind.
  • Demo: existing anon claim call now sends the type field.
  • Docs: AUTH.md Step 4 gets a "4a-alt. Claim via ID-JAG" subsection; agent-services README documents both shapes; README.md gets a new sequence diagram showing the two branches.

Smoke test

Step-up path (ceremony required):

  1. Pre-seed alice@example.com at the service via email-verif
  2. Anonymous register
  3. POST /claim with type: identity_assertion + ID-JAG → 200 with claim_attempt (user_code 428947)
  4. User completes ceremony at /claim → 200
  5. Agent polls /oauth2/token with claim grant → scope api.read api.write, v2 assertion's email: alice@example.com

Clean-match path (ID-JAG enough):
6. Fresh anonymous register
7. POST /claim with type: identity_assertion + ID-JAG (same (iss, sub), delegation now exists from step 5) → 200 + immediate v2 identity_assertion, no ceremony ✓

@devin-ai-integration

Copy link
Copy Markdown

@greptileai review

@greptile-apps

greptile-apps Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a second body shape to POST /agent/identity/claim, discriminated on type, so an anonymous registration can be bound to a user identity by presenting an ID-JAG directly rather than always requiring a user_code ceremony. When the ID-JAG is enough on its own (existing delegation or no email conflict) the claim completes atomically with a v2 identity_assertion; when the ID-JAG's email matches a different existing account, the endpoint falls back to the familiar claim_attempt ceremony block.

  • schemas.ts makes claimBody a z.discriminatedUnion on type (login_hint | identity_assertion); this is a breaking rename for existing callers who must now send \"type\": \"login_hint\".
  • store.ts adds completeAnonymousClaimViaIdJag for the atomic clean-match path and extends recordClaimAttempt to carry the id_jag triple, so completeClaim upserts the delegation for both step-up-via-/identity and step-up-via-/claim paths.
  • agent-auth.ts adds handleAnonymousClaimViaIdJag that runs verifyIdJagmatchOrProvision → branches on match kind; docs, CHANGELOG, and README sequence diagrams are updated to match.

Confidence Score: 3/5

The clean-match ID-JAG path in store.ts can be permanently blocked after any expired ceremony, and the step_up branch in agent-auth.ts silently overwrites an active in-flight ceremony — both need fixing before the new endpoint behaves as documented.

Two issues affect the new claim path's correctness: completeAnonymousClaimViaIdJag gates on status === pending_claim but that status persists after the user_code expires (since claim.attempt is never cleared), so a delegation-matching ID-JAG is blocked until the entire claim token expires rather than just until the ceremony window closes. Separately, handleAnonymousClaimViaIdJag calls recordClaimAttempt in the step_up branch without first checking whether an active ceremony is already running, so a concurrent or retried call can silently replace a ceremony the user is actively completing.

agent-services/src/store.ts (the pending_claim guard in completeAnonymousClaimViaIdJag) and agent-services/src/routes/agent-auth.ts (the step_up branch in handleAnonymousClaimViaIdJag) need the most attention.

Important Files Changed

Filename Overview
agent-services/src/routes/agent-auth.ts Adds handleAnonymousClaimViaIdJag — dispatches on type, verifies ID-JAG, branches on matchOrProvision result; the step_up path can silently overwrite an active ceremony (no pending_claim guard before recordClaimAttempt), and the email-immutability bypass for in-flight ceremonies is unguarded (covered in prior threads).
agent-services/src/store.ts Adds completeAnonymousClaimViaIdJag and extends recordClaimAttempt/completeClaim; the pending_claim guard in completeAnonymousClaimViaIdJag does not account for expired user_codes, permanently blocking the clean-match path after any initiated ceremony.
agent-services/src/schemas.ts Refactors claimBody into a z.discriminatedUnion on type with loginHintClaimBody and idJagClaimBody sub-schemas; clean and correct.
agent-services/src/routes/home.ts Updates anonClaim to use type: login_hint and renames email to login_hint correctly; updateAnonClaimPreview still uses type: email (invalid discriminator) and the old email key (covered in prior thread).
AUTH.md Adds 4a-alt section documenting both ID-JAG claim shapes with accurate request/response examples; failure table is correctly updated; minor doc inconsistency about interaction_required vs 200 covered in prior thread.
agent-services/README.md Documents both login_hint and identity_assertion shapes with implementation steps and response examples; step_up description mentions 401 interaction_required which is inconsistent with the actual 200 response (prior thread).
README.md Adds a sequence diagram for the two ID-JAG claim branches (clean-match and step_up); accurate and matches the implementation.
CHANGELOG.md Adds v0.7.0 entry documenting both new response shapes and calling out the breaking type discriminator requirement; accurate.
package.json Version bumped from 0.6.0 to 0.7.0 consistent with the breaking change in claimBody.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[POST /agent/identity/claim] --> B{Parse claimBody\ndiscriminatedUnion on type}
    B -->|invalid| C[400 invalid_request]
    B --> D{Lookup registration\nby claim_token hash}
    D -->|not found| E[401 invalid_claim_token]
    D -->|expired| F[410 claim_expired]
    D -->|claimed| G[409 claimed_or_in_flight]
    D --> H{type?}
    H -->|identity_assertion| I{kind === anonymous?}
    I -->|no| J[409 claimed_or_in_flight]
    I -->|yes| K[verifyIdJag]
    K -->|auth_time error| L[401 login_required]
    K -->|other error| M[400 verifier error]
    K -->|ok| N[matchOrProvision]
    N -->|step_up_required| O[recordClaimAttempt\nwrite id_jag triple]
    O --> P[200 status: initiated\nclaim_attempt block]
    N -->|match| Q[completeAnonymousClaimViaIdJag]
    Q -->|ceremony_in_flight\npending_claim status| R[409 ceremony_in_flight]
    Q -->|ok| S[signServiceIdJag]
    S --> T[200 status: claimed\nidentity_assertion v2]
    H -->|login_hint| U[classifyLoginHint]
    U -->|invalid| V[400 invalid_login_hint]
    U -->|ok| W[recordClaimAttempt]
    W --> X[200 status: initiated\nclaim_attempt block]
Loading

Reviews (18): Last reviewed commit: "Bump package.json to 0.7.0 to match CHAN..." | Re-trigger Greptile

Comment thread agent-services/src/routes/home.ts
Comment thread AUTH.md Outdated
Comment thread agent-services/src/store.ts
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch from 81dfe27 to ff431df Compare June 4, 2026 00:06
@m0tzy m0tzy force-pushed the madison/id-jag-step-up branch 2 times, most recently from 54deca0 to 8cae8f2 Compare June 4, 2026 00:12
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch from ff431df to 2870085 Compare June 4, 2026 00:12
@m0tzy m0tzy force-pushed the madison/id-jag-step-up branch from 8cae8f2 to e624e5f Compare June 4, 2026 00:14
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch 2 times, most recently from acd4727 to 8d3c948 Compare June 4, 2026 00:32
@m0tzy m0tzy force-pushed the madison/id-jag-step-up branch from e624e5f to 28ac6e1 Compare June 4, 2026 00:32
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch from 8d3c948 to 37cc113 Compare June 4, 2026 00:43
@m0tzy m0tzy force-pushed the madison/id-jag-step-up branch from 090e1b6 to 617a4fb Compare June 4, 2026 03:43
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch 2 times, most recently from 46b24a3 to 68784d9 Compare June 4, 2026 03:44
@m0tzy m0tzy force-pushed the madison/id-jag-step-up branch 2 times, most recently from 554ae3a to 06058af Compare June 4, 2026 03:49
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch from 68784d9 to 3bb0904 Compare June 4, 2026 03:49
@m0tzy

m0tzy commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@m0tzy m0tzy force-pushed the madison/id-jag-step-up branch from 1df9a5a to 47ac80d Compare June 4, 2026 04:17
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch from 583542a to 35ae83b Compare June 4, 2026 04:17
@m0tzy

m0tzy commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

1 similar comment
@m0tzy

m0tzy commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@m0tzy m0tzy marked this pull request as draft June 5, 2026 16:33
@m0tzy m0tzy marked this pull request as ready for review June 5, 2026 16:33
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch from bfaa624 to b6b9cc7 Compare June 5, 2026 16:33
@m0tzy m0tzy marked this pull request as draft June 5, 2026 16:43
@m0tzy m0tzy marked this pull request as ready for review June 5, 2026 16:43
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch from b6b9cc7 to e238dad Compare June 5, 2026 16:43
m0tzy added a commit that referenced this pull request Jun 5, 2026
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>
Base automatically changed from madison/id-jag-step-up to main June 5, 2026 16:48
@m0tzy

m0tzy commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@m0tzy m0tzy marked this pull request as draft June 5, 2026 21:21
@m0tzy m0tzy marked this pull request as ready for review June 5, 2026 21:21
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch from 59d363e to 8ce2b1d Compare June 5, 2026 21:21
@m0tzy

m0tzy commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@m0tzy m0tzy marked this pull request as draft June 5, 2026 22:27
@m0tzy m0tzy marked this pull request as ready for review June 5, 2026 22:27
Comment thread agent-services/src/routes/agent-auth.ts Outdated
m0tzy and others added 13 commits June 10, 2026 10:02
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>
Aligning with the actual merge date.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch from 601e524 to 81b6112 Compare June 10, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant