Skip to content

Claim anonymous registration via ID-JAG#12

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

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

Conversation

@m0tzy
Copy link
Copy Markdown
Collaborator

@m0tzy m0tzy commented Jun 3, 2026

  • 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
Copy link
Copy Markdown

greptile-apps Bot commented Jun 3, 2026

Greptile Summary

This PR adds a second body shape to POST /agent/identity/claim — an identity_assertion type that lets an anonymous registration be bound to a user via an ID-JAG, either atomically (clean match) or by falling back to a user_code ceremony (step-up). Schema, store, route handler, docs, and the demo client are all updated consistently.

  • schemas.ts converts claimBody to a z.discriminatedUnion on \"type\", requiring existing email-shape callers to add \"type\": \"email\" (breaking change documented in CHANGELOG).
  • store.ts adds completeAnonymousClaimViaIdJag for the atomic clean-match path and extends recordClaimAttempt with an optional idJag triple so completeClaim can upsert the delegation when the step-up ceremony is confirmed; the pending_claim guard prevents a competing ceremony from being stomped.
  • agent-auth.ts adds handleAnonymousClaimViaIdJag with email-immutability enforcement and correctly branches between the step-up and clean-match paths.

Confidence Score: 5/5

Safe to merge — the core claim path is correctly guarded and the previous review's concerns are addressed.

The email-immutability guard, pending_claim block, and ceremony_in_flight error path all work correctly together. The one data-consistency wrinkle — matchOrProvision creating a JIT user/delegation before the email check or ceremony_in_flight guard can reject the request — doesn't expose wrong data on the registration itself and mirrors the pre-existing behavior of the /agent/identity endpoint. Docs, schema, store, and route handler are all consistent with each other and the PR description.

agent-services/src/routes/agent-auth.ts — specifically the ordering of matchOrProvision relative to the email check in handleAnonymousClaimViaIdJag.

Important Files Changed

Filename Overview
agent-services/src/routes/agent-auth.ts Adds handleAnonymousClaimViaIdJag with email-immutability guard and dispatches on the new discriminator. The matchOrProvision call creates JIT side-effects (user + delegation) before the email check or ceremony_in_flight guard can reject the request.
agent-services/src/store.ts Adds completeAnonymousClaimViaIdJag, extends recordClaimAttempt with optional idJag, and broadens completeClaim's delegation condition from kind === "id_jag" to id_jag presence — all look correct.
agent-services/src/schemas.ts Converts claimBody from a plain object to a z.discriminatedUnion on "type" with emailClaimBody and idJagClaimBody — schema is correct.
agent-services/src/routes/home.ts Adds type: "email" to both the preview helper and the actual anonClaim() request body — both now match the updated discriminated union schema.
AUTH.md Documents the new "4a-alt. Claim via ID-JAG" subsection with both success shapes and failure codes, consistent with the implementation.
agent-services/README.md Adds the "Claim via ID-JAG" section documenting both terminal shapes and the anonymous-only restriction — matches the implementation.

Sequence Diagram

sequenceDiagram
    actor User
    participant Agent
    participant Service

    Agent->>Service: "POST /agent/identity { type: anonymous }"
    Service-->>Agent: 200 (v1 identity_assertion, claim_token)

    Note over Agent: operates pre-claim

    User-->>Agent: Signs in at provider — ID-JAG obtained

    Agent->>Service: "POST /agent/identity/claim<br/>{ type: identity_assertion, claim_token, assertion }"
    Service->>Service: verifyIdJag → matchOrProvision

    alt Clean match (delegation exists or JIT, no email conflict)
        Service-->>Agent: "200 { status: claimed, identity_assertion v2 }"
        Note over Agent: exchange v2 at /oauth2/token (jwt-bearer)
    else Step-up required (email matches existing different account)
        Service-->>Agent: "200 { status: initiated, claim_attempt: user_code + verification_uri }"
        Agent-->>User: Surface user_code + verification_uri
        User->>Service: GET verification_uri → POST /claim/complete
        Note over Service: bind anonymous reg to user + upsert (iss,sub) delegation
        loop poll until claimed
            Agent->>Service: POST /oauth2/token (claim grant)
            Service-->>Agent: 200 (access_token + v2 identity_assertion)
        end
    end
Loading

Reviews (16): Last reviewed commit: "Again with feeling" | 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
Copy link
Copy Markdown
Collaborator Author

m0tzy commented Jun 4, 2026

@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
Copy link
Copy Markdown
Collaborator Author

m0tzy commented Jun 4, 2026

@greptile

1 similar comment
@m0tzy
Copy link
Copy Markdown
Collaborator Author

m0tzy commented Jun 4, 2026

@greptile

@m0tzy m0tzy marked this pull request as draft June 4, 2026 21:51
@m0tzy m0tzy marked this pull request as ready for review June 4, 2026 21:51
@m0tzy m0tzy force-pushed the madison/claim-via-id-jag branch from deb9929 to bfaa624 Compare June 4, 2026 21:51
@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
Copy link
Copy Markdown
Collaborator Author

m0tzy commented Jun 5, 2026

@greptile

m0tzy and others added 7 commits June 5, 2026 14:21
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>
@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
Aligning with the actual merge date.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@m0tzy
Copy link
Copy Markdown
Collaborator Author

m0tzy commented Jun 5, 2026

@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
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