Skip to content

Split service_auth out from identity_assertion; soft-confirm on /claim#15

Open
m0tzy wants to merge 1 commit into
mainfrom
madison/service-auth-rename
Open

Split service_auth out from identity_assertion; soft-confirm on /claim#15
m0tzy wants to merge 1 commit into
mainfrom
madison/service-auth-rename

Conversation

@m0tzy
Copy link
Copy Markdown
Collaborator

@m0tzy m0tzy commented Jun 6, 2026

  • 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.

Also replaces the /claim hard wrong-account 403 with three soft confirmation advisories (hint_mismatch, first_time_provider, first_time_account) rendered above the user_code form. 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_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
Copy link
Copy Markdown

greptile-apps Bot commented Jun 6, 2026

Greptile Summary

This PR promotes the email-based registration path out of identity_assertion into a top-level service_auth type with a CIBA-style login_hint body, and replaces the hard wrong-account 403 on /claim with soft advisory prompts rendered above the user_code form.

  • service_auth type: { \"type\": \"service_auth\", \"login_hint\": \"<email>\" } replaces { \"type\": \"identity_assertion\", \"assertion_type\": \"verified_email\", \"assertion\": \"<email>\" }. Discovery, schemas, store types, and all documentation are updated consistently.
  • /claim advisory system: computeAdvisories emits hint_mismatch, first_time_provider, and first_time_account warnings above the form; the wrong-account hard block is removed from both the GET and POST handlers, making user_code entry the sole consent gate regardless of which account is signed in.
  • Discovery slim: identity_assertion.assertion_types_supported drops verified_email; identity_types_supported gains service_auth; agents are told to send service_auth and anonymous without consulting discovery, falling back on *_not_enabled errors.

Confidence Score: 3/5

The 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.

Security Review

  • Wrong-account claim gate removed (agent-services/src/routes/claim.ts, POST /claim/complete): The hard 403 that blocked a different-account user from completing the ceremony is gone. An attacker who intercepts the user_code can now sign in as themselves and submit the code to bind the agent registration to their own account. The CHANGELOG explicitly documents this as an intentional trade-off, but it is a real weakening of the user_code interception model — the protection now depends entirely on the legitimate user reading the hint_mismatch advisory and choosing not to proceed.

Important Files Changed

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 }"
Loading

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security 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."

Comment on lines +11 to 14
const serviceAuthBody = z.object({
type: z.literal("service_auth"),
login_hint: z.email(),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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!

Comment on lines +139 to +148
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 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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!

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