Skip to content

Rename verified_email → service_auth, rework how the claim email is stored + managed#15

Open
m0tzy wants to merge 3 commits into
mainfrom
madison/service-auth-rename
Open

Rename verified_email → service_auth, rework how the claim email is stored + managed#15
m0tzy wants to merge 3 commits into
mainfrom
madison/service-auth-rename

Conversation

@m0tzy

@m0tzy m0tzy commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator
  • QA'd in a deployed environment

Summary

Promotes the email-based registration path out from under identity_assertion and into a top-level service_auth registration type with a CIBA-style login_hint body. The previous shape filed verified-email under identity_assertion like the agent was asserting something, but the agent is really just hinting at who the user is — the service does the verifying. CIBA's vocabulary fits, so this aligns with it. See the v0.7.0 CHANGELOG entry for the full wire diff.

Internal rework of how the hint is stored: a new LoginHint = { kind: "email"; value: string } lives on the RegistrationClaimAttempt (not the claim itself), so a corrected re-mint via /agent/identity/claim can carry a corrected hint without being locked to whatever the first attempt bound. The route classifies the wire string at the edge via classifyLoginHint and returns a new invalid_login_hint error if it doesn't look like an email. The user-facing wrong-account 403 on /claim is preserved — the signed-in user must match the current attempt's login_hint to complete the ceremony.

Discovery slim: agent_auth.identity_assertion.assertion_types_supported drops "verified_email" (ID-JAG only now); identity_types_supported gains "service_auth". AUTH.md Step 2 narrows the cross-check guidance — only identity_assertion needs to consult discovery; service_auth and anonymous are send-and-fall-back on *_not_enabled.

🤖 Generated with Claude Code

Promotes the email-based registration path out from under identity_assertion
into a top-level `service_auth` type with a CIBA-style `login_hint` body, and
replaces the /claim wrong-account 403 with advisory prompts (hint_mismatch,
first_time_provider, first_time_account) that surface above the user_code
form without blocking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR promotes the email-based registration path out of identity_assertion into a new top-level service_auth type with a CIBA-shaped login_hint body, and moves the hint off RegistrationClaim.email and onto per-attempt RegistrationClaimAttempt.login_hint so re-mints can carry a corrected address.

  • verified_email assertion type and verified_email_not_enabled error are removed; the wire shape changes to { type: \"service_auth\", login_hint: \"<email>\" } with classifyLoginHint sniffing format at the edge.
  • A new advisory system (first_time_account, first_time_provider) surfaces above the claim form instead of the old monolithic promptCopy function.
  • All documentation (AUTH.md, READMEs, CHANGELOG) is updated consistently with the new vocabulary.

Confidence Score: 4/5

Safe to merge after fixing the ID-JAG step-up wrong-account gap in store.ts.

The findOrCreateIdJagRegistration function was not updated alongside the RegistrationClaim refactor: it still writes email to a field that no longer exists on the type, and crucially does not set login_hint on the RegistrationClaimAttempt it mints. Because hintMismatch in claim.ts reads attempt.login_hint, this field being absent means the wrong-account check always passes for ID-JAG step-up flows — any signed-in user can complete the ceremony and bind the matched delegation to their own account. All other flows (service_auth, anonymous) correctly populate login_hint and are unaffected.

agent-services/src/store.ts — specifically the findOrCreateIdJagRegistration function's claim/attempt construction block.

Important Files Changed

Filename Overview
agent-services/src/store.ts Refactors RegistrationKind, RegistrationClaim, and adds LoginHint/classifyLoginHint — but findOrCreateIdJagRegistration was not updated: it still assigns the now-orphaned email to RegistrationClaim and does not set login_hint on the attempt, silently breaking wrong-account enforcement for ID-JAG step-up flows.
agent-services/src/routes/claim.ts Replaces promptCopy with a structured Advisory system and moves wrong-account check to hintMismatch; correct for service_auth and anonymous flows, but the ID-JAG step-up gap in store.ts means hintMismatch always returns false for that flow here too.
agent-services/src/routes/agent-auth.ts Cleanly removes email-assertion path and wires in handleServiceAuth; classifyLoginHint gating at the edge is correct; comment for service_auth re-init still says 'supplied email must match' which is no longer enforced (intentional per PR).
agent-services/src/schemas.ts Removes emailAssertionBody, adds serviceAuthBody with z.string().min(1) for login_hint; format sniffing delegated to classifyLoginHint at the handler layer.
agent-services/src/routes/well-known.ts Correctly adds service_auth to identity_types_supported and drops verified_email from assertion_types_supported.
agent-services/src/routes/token.ts Renames email_verification to service_auth in sourceForRegistrationKind; straightforward rename, no logic changes.

Sequence Diagram

sequenceDiagram
    participant Agent
    participant Service
    participant User

    Note over Agent,Service: service_auth flow (new)
    Agent->>Service: "POST /agent/identity { type: service_auth, login_hint: email }"
    Service->>Service: "classifyLoginHint → LoginHint { kind: email, value }"
    Service-->>Agent: "200 { claim_token, claim: { user_code, verification_uri } }"
    Agent-->>User: Surface user_code + verification_uri

    User->>Service: "GET /claim?claim_attempt_token=..."
    Service->>Service: hintMismatch(attempt.login_hint, user)?
    alt hint matches signed-in user
        Service-->>User: Render form + advisories
        User->>Service: "POST /claim/complete { user_code }"
        Service->>Service: completeClaim()
        Service-->>User: All set
    else hint mismatch
        Service-->>User: 403 Wrong account
    end

    Agent->>Service: POST /oauth2/token (claim grant poll)
    Service-->>Agent: access_token + identity_assertion

    Note over Agent,Service: ID-JAG step-up (gap in this PR)
    Agent->>Service: "POST /agent/identity { type: identity_assertion, assertion: JWT }"
    Service->>Service: matchOrProvision → step_up_required
    Service-->>Agent: "401 interaction_required { claim_token, claim: { user_code, ... } }"
    Note over Service: attempt.login_hint NOT set → hintMismatch always false
    User->>Service: GET /claim (any signed-in user can complete)
Loading

Reviews (4): Last reviewed commit: "Restore /claim wrong-account 403; rework..." | Re-trigger Greptile

Comment thread agent-services/src/routes/claim.ts
Comment thread agent-services/src/schemas.ts
Comment thread agent-services/src/routes/claim.ts
@m0tzy m0tzy changed the title Split service_auth out from identity_assertion; soft-confirm on /claim Rename verified_email → service_auth, split out from identity_assertion Jun 8, 2026
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@m0tzy m0tzy changed the title Rename verified_email → service_auth, split out from identity_assertion Rename verified_email → service_auth, rework how the claim email is stored + managed Jun 8, 2026
Reverts the soft-advisory experiment from earlier in this branch. The
wrong-account hard reject is back: both GET /claim and POST /claim/complete
return 403 when the current attempt's login_hint doesn't match the signed-
in user. `hint_mismatch` advisory removed; `first_time_account` and
`first_time_provider` stay as informational notices.

Internal: `claim.email: string` → `attempt.login_hint: LoginHint = { kind:
"email", value }`. Per-attempt (not claim-level) so a re-mint via
`/agent/identity/claim` can correct the hint without being locked to the
first attempt. `/agent/identity` classifies the wire string at the edge
via `classifyLoginHint` and returns `invalid_login_hint` for anything that
isn't an email shape.

The `/agent/identity/claim` email-immutability check is gone — corrected
re-mints are the legitimate retry path, and the user-facing 403 is what
defends the user_code-interception case.

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

m0tzy commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

@mthadley mthadley self-requested a review June 9, 2026 20:32
Comment thread AUTH.md
3. **You have neither** → [anonymous](#anonymous). Claim ceremony optional; deferred until the user wants to take ownership.

Before sending: cross-check your choice against the `agent_auth` block. If the matching `*_supported` array doesn't list your method, this service won't accept that registration shape — pick another or stop.
For `identity_assertion`, cross-check that your assertion type is in `agent_auth.identity_assertion.assertion_types_supported` — trust setup isn't enumerable by trial, so a missing entry means stop. For `service_auth` and `anonymous`, `identity_types_supported` is informational — send the body and fall back on the `*_not_enabled` error if the service opted out.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
For `identity_assertion`, cross-check that your assertion type is in `agent_auth.identity_assertion.assertion_types_supported` — trust setup isn't enumerable by trial, so a missing entry means stop. For `service_auth` and `anonymous`, `identity_types_supported` is informational — send the body and fall back on the `*_not_enabled` error if the service opted out.
For `identity_assertion`, check that your assertion type is in `agent_auth.identity_assertion.assertion_types_supported`, if not listed then stop. For `service_auth` and `anonymous`, `identity_types_supported` is informational — send the body and fall back on the `*_not_enabled` error if the service opted out.

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.

2 participants