Split service_auth out from identity_assertion; soft-confirm on /claim#15
Split service_auth out from identity_assertion; soft-confirm on /claim#15m0tzy wants to merge 1 commit into
Conversation
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 SummaryThis PR promotes the email-based registration path out of
Confidence Score: 3/5The renaming and documentation changes are consistent and safe; the security model change at /claim is intentional but meaningfully weakens account-binding protection and needs explicit approval. The bulk of the diff — type renames, schema restructuring, discovery metadata, and documentation — is internally consistent and low-risk. The consequential change is in claim.ts: removing the wrong-account hard block means a user_code interceptor can now complete the ceremony as themselves rather than being rejected. The CHANGELOG acknowledges this, but the practical effect is that user_code secrecy becomes the only line of defense rather than a secondary one. The login_hint: z.email() schema also contradicts the CHANGELOG's stated extensibility intent for the field. agent-services/src/routes/claim.ts deserves the most scrutiny — specifically the removal of the wrong-account check from POST /claim/complete and the advisory ordering logic in computeAdvisories.
|
| Filename | Overview |
|---|---|
| agent-services/src/routes/claim.ts | Removes the hard wrong-account 403 from both GET and POST /claim, replacing it with a soft advisory system; any signed-in user with the correct user_code can now complete the claim as themselves. |
| agent-services/src/schemas.ts | Replaces emailAssertionBody with serviceAuthBody using z.email() for login_hint — contradicts CHANGELOG's stated intent of an extensible, untyped CIBA hint string. |
| agent-services/src/routes/agent-auth.ts | Straightforward handler rename from handleEmailAssertion to handleServiceAuth; routes the new service_auth type and calls createServiceAuthRegistration. |
| agent-services/src/store.ts | Type renames throughout: RegistrationKind email_verification → service_auth, Credential.source updated to match; createEmailVerificationRegistration renamed to createServiceAuthRegistration. |
| agent-services/src/routes/well-known.ts | Adds service_auth to identity_types_supported and drops verified_email from assertion_types_supported, keeping discovery metadata aligned with the new type structure. |
| agent-services/src/routes/token.ts | Updates sourceForRegistrationKind type signatures from email_verification to service_auth; no logic change. |
| agent-services/src/routes/home.ts | Updates the interactive demo UI text and JS preview/fetch payloads to reflect the new service_auth body shape. |
| AUTH.md | Decision tree, Step 3 copy, error table, and discovery examples updated consistently; identity_assertion + email references migrated to service_auth throughout. |
| CHANGELOG.md | Adds v0.7.0 entry covering the service_auth promotion, advisory system, and explicitly documents the security trade-off of the soft hint_mismatch. |
| README.md | Discovery example and sequence diagram updated to reflect service_auth body shape; no logic changes. |
| agent-providers/README.md | Discovery snippet updated to drop verified_email and add service_auth; consistent with other documentation changes. |
| agent-services/README.md | Implementation guide updated throughout: service_auth request shape, response registration_type, claim ceremony description, and audit event table. |
Sequence Diagram
sequenceDiagram
participant Agent
participant Service
participant User
Note over Agent,Service: Step 3 — service_auth registration
Agent->>Service: "POST /agent/identity<br/>{ type: service_auth, login_hint: email }"
Service-->>Agent: "200 { registration_id, claim_token,<br/>claim: { user_code, verification_uri, interval } }"
Note over Agent,User: Step 4b — hand off to user
Agent->>User: Surface user_code + verification_uri
Note over User,Service: Step 4 — claim ceremony
User->>Service: "GET /claim?claim_attempt_token=..."
Service-->>User: HTML form + advisories
User->>Service: "POST /claim/complete { user_code }"
Service-->>User: 200 All set
Note over Agent,Service: Step 4c — poll for completion
Agent->>Service: "POST /oauth2/token<br/>{ grant_type: claim, claim_token }"
Service-->>Agent: "200 { access_token, identity_assertion }"
Reviews (1): Last reviewed commit: "Split service_auth out from identity_ass..." | Re-trigger Greptile
| return; | ||
| } | ||
|
|
||
| const result = completeClaim(registration, parsed.value.user_code, user); |
There was a problem hiding this comment.
Wrong-account enforcement removed from claim completion
completeClaim now runs unconditionally regardless of whether the signed-in user matches the registration's login_hint. The advisory shown on the GET page is purely informational — any signed-in user who possesses a valid user_code can complete the ceremony and bind the registration to their own account. An attacker who intercepts the user_code (e.g., from a compromised channel or a phishing page that mirrors the verification_uri) can now claim the agent as themselves, whereas the old hard 403 prevented this entirely. The CHANGELOG acknowledges the trade-off, but this warrants explicit sign-off since it changes the attacker model from "code interception is not enough" to "code interception is sufficient."
| const serviceAuthBody = z.object({ | ||
| type: z.literal("service_auth"), | ||
| login_hint: z.email(), | ||
| }); |
There was a problem hiding this comment.
The CHANGELOG documents
login_hint as "an untyped string — service sniffs format, leaving room for phone numbers etc. later," matching CIBA's own definition of the field as an opaque hint. Using z.email() here hard-rejects anything that isn't an RFC 5322 email address today, making the stated extensibility path (phone numbers) a breaking change. z.string().min(1) preserves the CIBA-aligned semantics while still rejecting empty hints; format sniffing can live in the handler.
| const serviceAuthBody = z.object({ | |
| type: z.literal("service_auth"), | |
| login_hint: z.email(), | |
| }); | |
| const serviceAuthBody = z.object({ | |
| type: z.literal("service_auth"), | |
| login_hint: z.string().min(1), | |
| }); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| let anyPriorClaim = false; | ||
| for (const r of registrations.values()) { | ||
| if (r.user_id === user.id && r.claimed_at && r.id !== registration.id) { | ||
| anyPriorClaim = true; | ||
| break; | ||
| } | ||
| } | ||
| if (!anyPriorClaim) { | ||
| out.push({ kind: "first_time_account", userEmail: user.email }); | ||
| } |
There was a problem hiding this comment.
first_time_account fires for the wrong-account user alongside hint_mismatch
When a hint_mismatch is present (signed-in user differs from login_hint) and the current user has never claimed an agent, both advisories are emitted. The user sees "The agent hinted this was for alice@personal…" immediately followed by "This is the first agent being linked to alice@work." The second advisory frames the situation as a normal first-time setup, which can nudge a confused user toward accepting rather than pausing to switch accounts. Suppressing first_time_account when hint_mismatch is already in out would keep the messaging unambiguous about what needs correcting.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Promotes the email-based registration path out from under
identity_assertionand into a top-levelservice_authregistration type with a CIBA-stylelogin_hintbody. The previous shape filed verified-email underidentity_assertionlike 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.Also replaces the
/claimhard wrong-account 403 with three soft confirmation advisories (hint_mismatch,first_time_provider,first_time_account) rendered above theuser_codeform. The form itself is still the consent gate — the advisories just name what's actually happening before the user types the code. UX win for users with multiple accounts at the service (alice@personal vs alice@work no longer requires sign-out); security trade-off called out inline in the changelog.Discovery slim:
agent_auth.identity_assertion.assertion_types_supporteddrops"verified_email"(ID-JAG only now);identity_types_supportedgains"service_auth". AUTH.md Step 2 narrows the cross-check guidance — onlyidentity_assertionneeds to consult discovery;service_authandanonymousare send-and-fall-back on*_not_enabled.🤖 Generated with Claude Code