feat(mcp): distribution polish for agent-first onboarding: CIMD, plugin start skill, bridge hint (#1814 PR 4) (#1866)
* feat(mcp): distribution polish for agent-first onboarding: CIMD, plugin start skill, bridge hint Fourth PR of agent-first onboarding (#1814). - The OAuth AS metadata advertises client_id_metadata_document_supported next to the existing `none` token auth, the pair Claude.ai, Claude Code and Codex look for to use CIMD instead of registering a DCR client per connection. authorize/token never keyed on client_id (the redirect-URI allowlist is the trust boundary), so nothing else changes; DCR stays for ChatGPT. - The plugin's start skill no longer sends a user without an account to the website: the /mcp OAuth screen creates the account, and a NO_COMPANY_YET briefing failure routes to the onboarding skill and accounted_create_company. README updated to match. - `npx accounted-mcp` without ACCOUNTED_API_KEY prints the OAuth alternative (Claude Code, Codex, Claude.ai connector) and that the account can be created on the sign-in screen; package README too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(oauth): do not advertise CIMD until redirect URIs are matched against the client document CodeRabbit on #1866: advertising client_id_metadata_document_supported makes Claude and Codex send URL client_ids and expects an exact redirect_uri match against that document; the authorize endpoint only checks the global allowlist and never fetches client metadata. The flag is withheld until an SSRF-safe, cached CIMD fetch with exact redirect matching exists. DCR stays the registration path (stateless, so free). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
31e0cd6e05
commit
e92365b86e
@@ -0,0 +1,34 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { GET } from '../route'
|
||||
|
||||
describe('GET /.well-known/oauth-authorization-server', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('NEXT_PUBLIC_APP_URL', 'https://app.example.test')
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
it('does not advertise CIMD until redirect URIs are validated against the client document', async () => {
|
||||
// Advertising client_id_metadata_document_supported makes Claude and
|
||||
// Codex send URL client_ids and expects an exact redirect_uri match
|
||||
// against that document; the authorize endpoint only checks the global
|
||||
// allowlist today, so the flag must stay off (see the route comment).
|
||||
const res = await GET(new Request('https://app.example.test/.well-known/oauth-authorization-server'))
|
||||
expect(res.status).toBe(200)
|
||||
const body = await res.json()
|
||||
expect(body.client_id_metadata_document_supported).toBeUndefined()
|
||||
expect(body.token_endpoint_auth_methods_supported).toContain('none')
|
||||
// DCR (stateless register endpoint) remains the registration path.
|
||||
expect(body.registration_endpoint).toBe('https://app.example.test/api/mcp-oauth/register')
|
||||
expect(body.code_challenge_methods_supported).toEqual(['S256'])
|
||||
expect(body.authorization_response_iss_parameter_supported).toBe(true)
|
||||
})
|
||||
|
||||
it('does not enumerate write scopes in public discovery', async () => {
|
||||
const res = await GET(new Request('https://app.example.test/.well-known/oauth-authorization-server'))
|
||||
const body = await res.json()
|
||||
expect(body.scopes_supported).toContain('mcp')
|
||||
expect(body.scopes_supported.some((s: string) => s.endsWith(':write'))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -23,6 +23,16 @@ export async function GET(request: Request) {
|
||||
grant_types_supported: ['authorization_code', 'refresh_token'],
|
||||
code_challenge_methods_supported: ['S256'],
|
||||
token_endpoint_auth_methods_supported: ['none', 'client_secret_post'],
|
||||
// Client ID Metadata Documents (MCP auth spec 2025-11-25) are deliberately
|
||||
// NOT advertised yet. Advertising the flag makes Claude.ai, Claude Code
|
||||
// and Codex send an HTTPS URL as client_id, and the spec then expects the
|
||||
// authorization server to fetch that document and match redirect_uri
|
||||
// exactly against its redirect_uris. Our authorize endpoint validates
|
||||
// redirect_uri against the global allowlist only (lib/auth/oauth-allowlist.ts)
|
||||
// and never fetches client metadata, so advertising CIMD would claim a
|
||||
// check we do not perform. The stateless register endpoint makes DCR
|
||||
// free for us, so nothing is lost by waiting: add the flag together with
|
||||
// an SSRF-safe, cached CIMD fetch and exact redirect matching.
|
||||
// RFC 9207: the authorize endpoint includes `iss` in every authorization
|
||||
// response (success and error) so clients can detect mix-up attacks.
|
||||
authorization_response_iss_parameter_supported: true,
|
||||
|
||||
Reference in New Issue
Block a user