diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e614a5a..3a26d03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,15 @@ permissions: contents: read jobs: + idp-publisher: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24.22.1 + - run: node --test scripts/sync-world-id-auth.test.mjs + spellcheck: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/sync-world-id-auth.yml b/.github/workflows/sync-world-id-auth.yml new file mode 100644 index 0000000..8f1eddd --- /dev/null +++ b/.github/workflows/sync-world-id-auth.yml @@ -0,0 +1,124 @@ +name: Sync IDP docs + +on: + schedule: + - cron: "17 * * * *" + workflow_dispatch: + inputs: + source_ref: + description: "temporal-apps branch, tag, or commit to publish for review" + required: true + default: main + type: string + +permissions: + contents: read + +concurrency: + group: sync-world-id-auth + cancel-in-progress: false + +jobs: + sync: + if: github.repository == 'worldcoin/developer-docs' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: developer-docs + steps: + - name: Check publishing credentials + working-directory: ${{ github.workspace }} + env: + APP_ID: ${{ vars.IDP_DOCS_APP_ID }} + APP_PRIVATE_KEY: ${{ secrets.IDP_DOCS_APP_PRIVATE_KEY }} + run: | + if [ -z "$APP_ID" ] || [ -z "$APP_PRIVATE_KEY" ]; then + echo "::error::Configure IDP_DOCS_APP_ID and IDP_DOCS_APP_PRIVATE_KEY before running IDP publishing." + exit 1 + fi + + - name: Check out publishing code + uses: actions/checkout@v4 + with: + ref: main + path: developer-docs + persist-credentials: false + + - name: Create read-only source token + id: source-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.IDP_DOCS_APP_ID }} + private-key: ${{ secrets.IDP_DOCS_APP_PRIVATE_KEY }} + owner: worldcoin + repositories: temporal-apps + permission-contents: read + + - name: Check out canonical guides + uses: actions/checkout@v4 + with: + repository: worldcoin/temporal-apps + ref: ${{ inputs.source_ref || 'main' }} + token: ${{ steps.source-token.outputs.token }} + path: _source/temporal-apps + persist-credentials: false + sparse-checkout: | + apps/idpapi/mcp/guides + apps/idpportal/public/assets + + - name: Record source commit + id: source + run: | + source_sha=$(git -C ../_source/temporal-apps rev-parse HEAD) + echo "sha=$source_sha" >> "$GITHUB_OUTPUT" + + - uses: actions/setup-node@v4 + with: + node-version: 24.22.1 + + - name: Generate pages + env: + SOURCE_COMMIT: ${{ steps.source.outputs.sha }} + run: node scripts/sync-world-id-auth.mjs --source-dir ../_source/temporal-apps --source-commit "$SOURCE_COMMIT" + + - name: Check generated output + env: + SOURCE_COMMIT: ${{ steps.source.outputs.sha }} + run: | + node scripts/sync-world-id-auth.mjs --source-dir ../_source/temporal-apps --source-commit "$SOURCE_COMMIT" --check + git diff --check -- world-id-auth images/world-id-auth .mintlify/world-id-auth-source.json + + - name: Create publishing token + id: docs-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.IDP_DOCS_APP_ID }} + private-key: ${{ secrets.IDP_DOCS_APP_PRIVATE_KEY }} + owner: worldcoin + repositories: developer-docs + permission-contents: write + permission-pull-requests: write + + - name: Open or update publishing PR + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ steps.docs-token.outputs.token }} + path: developer-docs + base: main + branch: codex/sync-world-id-auth + commit-message: sync IDP docs + title: sync IDP docs + draft: true + add-paths: | + world-id-auth/*.mdx + images/world-id-auth/mcp-federation.svg + .mintlify/world-id-auth-source.json + body: | + ## Summary + - Publish canonical guides from [temporal-apps](https://github.com/worldcoin/temporal-apps/tree/${{ steps.source.outputs.sha }}/apps/idpapi/mcp/guides). + - Review content and release status before merging. This PR does not deploy Auth or change MCP access controls. + + ## Testing + - Passed: deterministic regeneration check and `git diff --check`. + - Not run: full Mintlify build. diff --git a/.mintlify/AGENTS.md b/.mintlify/AGENTS.md new file mode 100644 index 0000000..6814226 --- /dev/null +++ b/.mintlify/AGENTS.md @@ -0,0 +1,44 @@ +# Documentation maintenance + +- Write all published pages, maintenance notes, commit messages, and PR descriptions in English. +- Follow the existing MDX, navigation, branding, and terminology conventions. Register pages in `docs.json`; reuse existing snippets and components when they fit. +- Document durable product behavior, public contracts, and necessary integration setup. Keep implementation walkthroughs, operational runbooks, and PR history out of published pages. +- Keep changes focused. Preserve coverage of the rest of the site when extending maintenance to IDP. Explain any necessary edits outside the affected product area. +- Keep commit subjects and PR titles short. Follow the repository's PR template. If there is no template, use `Summary` with one to three bullets and `Testing` with commands and outcomes or `Not run`. +- Respect the task's validation limits. State which checks passed, failed, or were skipped; never claim an unrun check passed. Do not publish or merge automatically. + +## IDP + +The new tab has eleven generated pages under `world-id-auth/`, starting with Quickstart. Read [the source and cutover map](world-id-auth-migration.md) and [publishing runbook](world-id-auth-publishing.md) before updating them. + +- Keep `worldcoin/temporal-apps/apps/idpapi/mcp/guides/*.md` as the canonical guide text shared by the Auth browser documentation, MCP, and the generated Mintlify pages. Make content changes in those Markdown files, then regenerate. The landing page in `apps/idpportal/docs.html` is excluded from this migration. +- Do not edit generated `world-id-auth/*.mdx` bodies directly. Keep publishing metadata and approved adaptations in `.mintlify/world-id-auth-pages.json` and `scripts/sync-world-id-auth.mjs`. Record the source commit in `.mintlify/world-id-auth-source.json` through the generator. +- Read relevant source diffs and the complete canonical guides. Preserve all sections, tables, examples, limitations, errors, and instructions when converting them to MDX. If code, canonical text, and generated pages disagree, identify the conflict for review and correct the source or converter. +- Keep the complete `coding-agent-setup.md` content, including `Agent instructions` and all sections after it. The Auth HTML renderer hides those sections; copying only rendered HTML loses content. +- Public adaptation of `world-id-users.md` as `use-world-id-with-your-agent.mdx` is approved for this migration. Adapt its authenticated-reader introduction for public readers. Keep the original MCP guide's access controls and the authentication requirements of account tools unchanged. +- Rewrite documentation links to the new routes. Keep issuer, discovery, authorization, token, portal, MCP, consent, connection-management, and resource-metadata URLs on the correct Auth host. Moving documentation does not move an API endpoint. +- Keep production, sandbox, staging, and local behavior separate. A source merge does not prove a production release. Use matching issuer, discovery, registration, and credentials for each environment; flag unclear release status. +- Preserve IDs, claims, scopes, method names, errors, example payloads, and existing section anchors. Flag legacy URLs that need redirects. Do not imply that Auth redirects or a documentation cutover have been deployed. +- Keep IDKit verification, Auth OIDC, the Auth user MCP, the Auth developer MCP, and the other documented World MCP services distinct. Their registrations, identities, tokens, and authorization contracts are not interchangeable. +- Never copy secrets, account data, internal runbooks, private operational URLs, or deployment configuration into public documentation. Source repository access does not make every source file suitable for publication. + +Review IDP changes in these source paths, plus shared-code changes that affect the public contract: + +```text +apps/idpapi/** +apps/idpportal/** +apps/idpweb/** +apps/idpworker/** +pkg/postgres/worldidp/** +infra/lib/idp/** +``` + +Include the source PR and relevant source files in documentation PRs. Leave pages unchanged when a source change has no developer-facing effect. + +## Maintenance configuration + +Use `.github/workflows/sync-world-id-auth.yml` for one-way publishing from `temporal-apps`. It polls hourly and supports manual dispatch, regenerates the allowlisted pages, and opens or updates a review PR on `codex/sync-world-id-auth`. Keep the source commit visible in that PR. Do not merge automatically or change global `docs.json` during synchronization. + +The workflow requires a scoped GitHub App and the configuration in [the publishing runbook](world-id-auth-publishing.md). Merging this workflow to `main` and configuring its credentials are activation steps. Do not describe automatic publishing as active until they are complete and a run succeeds. + +Mintlify Autopilot may continue maintaining other product areas. It must not independently rewrite generated IDP pages. Propose IDP content corrections in the canonical Markdown, or change the converter when the publishing adaptation needs correction. This workflow does not deploy Auth or its MCP services. diff --git a/.mintlify/world-id-auth-migration.md b/.mintlify/world-id-auth-migration.md new file mode 100644 index 0000000..946eed0 --- /dev/null +++ b/.mintlify/world-id-auth-migration.md @@ -0,0 +1,97 @@ +# IDP source and cutover map + +This maintenance note records the eleven-page migration into the `IDP` tab, starting with Quickstart. The pages are generated from the canonical Markdown in `temporal-apps`. This note is not a public integration guide or evidence of a deployed cutover. + +## Source snapshot + +- Repository: `worldcoin/temporal-apps`. +- Reviewed commit: [`9eedf2dee3a8fb1d864e02c59a98982a2c811e61`](https://github.com/worldcoin/temporal-apps/tree/9eedf2dee3a8fb1d864e02c59a98982a2c811e61). +- The full overview and eleven guide sources were reviewed before migration. The scope includes all ten existing public integration guides and the approved public adaptation of `world-id-users.md`. +- The [Auth landing page][overview-source] is excluded from the migration. Do not recreate an Overview page in the IDP tab. +- Keep the source Markdown canonical. The generated MDX is a publishing adaptation of that text. Content changes belong in `temporal-apps`; generated pages are not edited independently. +- The initial generated pages use the reviewed commit above. `.mintlify/world-id-auth-source.json` records the commit used by each regeneration; the historical review links below remain pinned to the original migration snapshot. + +The publication contract is documented in the [IdP API README](https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/README.md#developer-integration-guides). The [portal renderer](https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpportal/vite.config.ts) reads the guide Markdown, substitutes the environment issuer, and publishes browser pages. MCP embeds the guide sources in the API binary. Publishing here does not update either deployment. + +## Eleven-page coverage + +Destination paths below are relative to the repository root and resolve under `https://docs.world.org/` without the `.mdx` extension. + +| Canonical source | Destination | Required coverage | +| --- | --- | --- | +| [getting-started.md][quickstart-source] | `world-id-auth/quickstart.mdx` | How it works; four integration steps; device freshness and XAA consent distinction; environment values; IDKit differences; legacy sign-in deprecation; coding-agent entry point. | +| [app-registration.md][registration-source] | `world-id-auth/app-registration.mdx` | Eligibility; exact callbacks; client authentication; credentials and rotation; managed and legacy sectors; sharing and account linking; benefit listing publication. | +| [oidc.md][oidc-source] | `world-id-auth/connect-your-backend.mdx` | OIDC configuration; browser flow; token validation; client authentication; complete device profile and examples; expiry and rate limits; XAA; recovery; primary sign-in; limitations; troubleshooting. | +| [federation.md][federation-source] | `world-id-auth/connect-your-auth-server.mdx` | Auth-server configuration; MCP federation and diagram; identity mapping; recent authentication; verification and revocation boundaries. | +| [id-token.md][token-source] | `world-id-auth/id-tokens-and-identity.mdx` | Token response; decoded JWT; complete claims table; inbox email behavior; UserInfo; expiry and revocation limitations; safe subject storage and matching. | +| [step-up.md][step-up-source] | `world-id-auth/step-up.mdx` | Who may authorize; implementation; request parameters; Face Auth; default verification preferences; assurance; authorization boundaries; stale and duplicate operations; auth-server propagation; RFC 9470. | +| [human-in-the-loop.md][hitl-source] | `world-id-auth/human-in-the-loop.mdx` | Fresh human authentication; optional application-owned action approval; headless agents. Sidebar title: `Human in the loop`. | +| [xaa.md][xaa-source] | `world-id-auth/xaa.mdx` | Requirements; authorization-server metadata and configuration; app enrollment; agent connection; consent and access management. Sidebar title: `Cross App Access (XAA)`. | +| [world-id-users.md][users-source] | `world-id-auth/use-world-id-with-your-agent.mdx` | Authorized capabilities and limits; Orb lookup; benefits; partner claiming and confirmation; connection lifetime, refresh, revocation, and separate developer connections. | +| [coding-agent-setup.md][coding-source] | `world-id-auth/build-with-a-coding-agent.mdx` | Prompts; connection instructions; CLI examples; connection checks; complete agent instructions; portal permission; app registration and configuration; benefit listing publication. | +| [testing.md][testing-source] | `world-id-auth/testing.mdx` | Sandbox and playground; app setup; sandbox identity creation; a complete real flow; failure paths. | + +The publisher copies the canonical federation illustration from `apps/idpportal/public/assets/mcp-federation.svg` to `images/world-id-auth/mcp-federation.svg` and rewrites the guide's image URL. The manifest records that asset mapping and the source lock tracks its source. Keep diagram changes in `temporal-apps` so the Auth guide and Mintlify page use the same illustration. + +## Adaptation rules + +Preserve content rather than summarizing guide bodies. The deterministic converter in `scripts/sync-world-id-auth.mjs` applies the metadata and route mapping in `.mintlify/world-id-auth-pages.json`. Titles, frontmatter, MDX syntax, documentation links, and page layout may change to fit Mintlify. Preserve examples, tables, link destinations, and section meaning. Keep every subsection, including those not named in the sidebar. Fix content in the source Markdown and publishing adaptations in the manifest or converter, then regenerate. + +Use the complete Markdown for `coding-agent-setup`. The current Auth browser renderer cuts its content at `## Agent instructions`, hiding that section and everything after it. The raw Markdown and MCP guide include the complete content. Keep the agent instructions and later app-management sections available in the migrated page and its machine-readable form; update prompts to point to the complete migrated Markdown. + +`world-id-users` contains general guidance, not private account data. Its public adaptation is explicitly included. Replace the opening assumption that the reader already authenticated with language describing what happens after connection. Its publication does not remove authorization from the original MCP guide or `get_world_id_account`. + +## Environments and service URLs + +The Auth site substitutes `{{issuer}}` during each environment build. Resolve production examples to `https://auth.world.org` and keep sandbox examples on `https://sandbox.auth.world.org`. Do not infer staging or local production behavior from the source snapshot. Preserve documented local/test values only where they are already part of the public integration instructions. + +Only documentation links move to `docs.world.org/world-id-auth/`. Keep discovery, issuer, portal, OAuth, MCP, protected-resource metadata, consent, and connection-management URLs on their service origins. All credentials, registrations, and discovery metadata used together must belong to the same environment. Preserve standards and other external reference links. + +Keep Auth's `llms.txt`, public guide discovery, and the complete coding-agent instructions usable during transition. The new pages should be discoverable through the official site's existing documentation index and MCP. Do not confuse the read-only World Docs MCP with either Auth MCP service. + +## Legacy URL cutover map + +This is a mapping for a later Auth deployment. This migration does not install redirects on `auth.world.org`, change its CloudFront routes, remove old pages, or change MCP resource IDs. + +| Existing Auth path | Proposed official docs destination | +| --- | --- | +| `/docs` and `/docs/` | `/world-id-auth/quickstart` | +| `/docs/guides` and `/docs/guides/` | `/world-id-auth/quickstart` | +| `/docs/guides/getting-started` | `/world-id-auth/quickstart` | +| `/docs/guides/app-registration` | `/world-id-auth/app-registration` | +| `/docs/guides/oidc` | `/world-id-auth/connect-your-backend` | +| `/docs/guides/federation` | `/world-id-auth/connect-your-auth-server` | +| `/docs/guides/id-token` | `/world-id-auth/id-tokens-and-identity` | +| `/docs/guides/step-up` | `/world-id-auth/step-up` | +| `/docs/guides/human-in-the-loop` | `/world-id-auth/human-in-the-loop` | +| `/docs/guides/xaa` | `/world-id-auth/xaa` | +| `/docs/guides/coding-agent-setup` | `/world-id-auth/build-with-a-coding-agent` | +| `/docs/guides/coding-agent-setup.md` | `/world-id-auth/build-with-a-coding-agent.md` | +| `/docs/guides/testing` | `/world-id-auth/testing` | + +`world-id-users` has no existing public Auth page to redirect. Its new public destination is `/world-id-auth/use-world-id-with-your-agent`; the protected MCP resource remains separate. + +Preserve existing guide section fragments at their mapped destinations. In particular, Auth has legacy OIDC fragment aliases `sign-in-through-the-browser` and `sign-in-a-headless-agent-device-flow` in addition to links using the current heading slugs. Preserve those aliases alongside `authenticate-through-the-browser` and `connect-a-headless-agent-device-flow`. The excluded Auth landing page's section fragments have no migrated equivalent. + +## Maintenance handoff + +Use the one-way publisher in `.github/workflows/sync-world-id-auth.yml` and follow [the publishing runbook](world-id-auth-publishing.md). Canonical Markdown remains in `temporal-apps`. The workflow polls hourly or runs on manual dispatch, converts the allowlisted guides, and opens or updates a PR on `codex/sync-world-id-auth` with the source commit. Review is required before merging. The job does not rewrite global `docs.json` or deploy Auth and MCP services. + +Activation requires merging the workflow to `main`, granting a scoped GitHub App the required repository access, and setting its app ID and private-key secret. The presence of these files does not prove that synchronization is active. Existing Mintlify Autopilot maintenance may continue for other products, but must not independently edit these generated IDP pages. + +Review source changes against both the canonical guides and the implementation. Include the source PR and affected files in documentation PRs. Flag missing release evidence and source conflicts. Preserve maintenance of the rest of the site. + +Before retiring Auth's existing browser documentation, deploy the legacy URL handling and confirm that public Markdown, existing guide anchors, and MCP resources still resolve as intended. Keep the canonical `.md` files and the MCP's source consumption in place. These are cutover tasks, not claims made by this migration. + +[overview-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpportal/docs.html +[quickstart-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/getting-started.md +[registration-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/app-registration.md +[oidc-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/oidc.md +[federation-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/federation.md +[token-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/id-token.md +[step-up-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/step-up.md +[hitl-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/human-in-the-loop.md +[xaa-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/xaa.md +[users-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/world-id-users.md +[coding-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/coding-agent-setup.md +[testing-source]: https://github.com/worldcoin/temporal-apps/blob/9eedf2dee3a8fb1d864e02c59a98982a2c811e61/apps/idpapi/mcp/guides/testing.md diff --git a/.mintlify/world-id-auth-pages.json b/.mintlify/world-id-auth-pages.json new file mode 100644 index 0000000..c6a12a2 --- /dev/null +++ b/.mintlify/world-id-auth-pages.json @@ -0,0 +1,101 @@ +{ + "repository": "worldcoin/temporal-apps", + "sourceDirectory": "apps/idpapi/mcp/guides", + "issuer": "https://auth.world.org", + "pages": [ + { + "source": "getting-started.md", + "destination": "world-id-auth/quickstart.mdx", + "title": "Quickstart", + "sidebarTitle": "Quickstart", + "description": "Register an application, connect to the World ID issuer, and add step-up authentication.", + "environmentNote": true + }, + { + "source": "app-registration.md", + "destination": "world-id-auth/app-registration.mdx", + "title": "Register your application", + "sidebarTitle": "Register your application", + "description": "Register and manage your World ID OIDC application, credentials, callbacks, and benefit listing." + }, + { + "source": "oidc.md", + "destination": "world-id-auth/connect-your-backend.mdx", + "title": "Connect your backend", + "sidebarTitle": "Connect your backend", + "description": "Connect your backend to the World ID issuer with OpenID Connect (OIDC).", + "anchorAliases": { + "authenticate-through-the-browser": [ + "sign-in-through-the-browser" + ], + "connect-a-headless-agent-device-flow": [ + "sign-in-a-headless-agent-device-flow" + ] + } + }, + { + "source": "federation.md", + "destination": "world-id-auth/connect-your-auth-server.mdx", + "title": "Connect your auth server", + "sidebarTitle": "Connect your auth server", + "description": "Use the World ID issuer as an upstream provider for your auth server or MCP server." + }, + { + "source": "id-token.md", + "destination": "world-id-auth/id-tokens-and-identity.mdx", + "title": "ID tokens and identity", + "sidebarTitle": "ID tokens and identity", + "description": "Validate token claims, read UserInfo, and recognize accounts using the issuer and pairwise subject." + }, + { + "source": "step-up.md", + "destination": "world-id-auth/step-up.mdx", + "title": "Step-up authentication", + "sidebarTitle": "Step-up authentication", + "description": "Require recent World ID authentication before sensitive actions." + }, + { + "source": "human-in-the-loop.md", + "destination": "world-id-auth/human-in-the-loop.mdx", + "title": "Human in the loop", + "sidebarTitle": "Human in the loop", + "description": "Pause an agent workflow and require fresh World ID authentication before continuing." + }, + { + "source": "xaa.md", + "destination": "world-id-auth/xaa.mdx", + "title": "Cross App Access (XAA)", + "sidebarTitle": "Cross App Access (XAA)", + "description": "Connect agent hosts to reviewed apps with World ID identity delegation and user-controlled permissions." + }, + { + "source": "world-id-users.md", + "destination": "world-id-auth/use-world-id-with-your-agent.mdx", + "title": "Use World ID with your agent", + "sidebarTitle": "Use World ID with your agent", + "description": "Understand World ID agent permissions, discover benefits, and manage your connections.", + "publicUserGuide": true + }, + { + "source": "coding-agent-setup.md", + "destination": "world-id-auth/build-with-a-coding-agent.mdx", + "title": "Build with a coding agent", + "sidebarTitle": "Build with a coding agent", + "description": "Give your coding agent complete integration instructions and connect developer MCP tools." + }, + { + "source": "testing.md", + "destination": "world-id-auth/testing.mdx", + "title": "Test your integration", + "sidebarTitle": "Test your integration", + "description": "Test World ID integration in the sandbox, complete real flows, and check failure paths." + } + ], + "assets": [ + { + "source": "apps/idpportal/public/assets/mcp-federation.svg", + "destination": "images/world-id-auth/mcp-federation.svg", + "url": "{{issuer}}/portal/assets/mcp-federation.svg" + } + ] +} diff --git a/.mintlify/world-id-auth-publishing.md b/.mintlify/world-id-auth-publishing.md new file mode 100644 index 0000000..879ce25 --- /dev/null +++ b/.mintlify/world-id-auth-publishing.md @@ -0,0 +1,73 @@ +# IDP publishing + + + +Author IDP guides in `worldcoin/temporal-apps/apps/idpapi/mcp/guides/*.md`. The Auth portal and MCP keep consuming those files. `developer-docs` publishes a generated MDX representation of the same content. + +## Files and ownership + +- `scripts/sync-world-id-auth.mjs` converts the source Markdown without an LLM or runtime dependencies beyond Node.js. +- `.mintlify/world-id-auth-pages.json` allowlists the eleven source guides and records their existing destination slugs, publishing metadata, and asset mapping. +- `.mintlify/world-id-auth-source.json` records the source commit used for the generated pages and assets. +- `world-id-auth/*.mdx` are generated output. Change the canonical Markdown, manifest, or converter and regenerate instead of editing these pages directly. +- `.github/workflows/sync-world-id-auth.yml` proposes updates through a PR. Global navigation in `docs.json` remains separately maintained. + +The converter resolves `{{issuer}}` to the production issuer, preserves documented sandbox examples, maps documentation links, and escapes Markdown syntax for MDX. It retains the complete coding-agent instructions and applies the approved public introduction for `world-id-users.md`. That public adaptation does not change MCP authentication or account-tool permissions. See [the source and cutover map](world-id-auth-migration.md) for coverage and URL requirements. + +The federation SVG is copied from `temporal-apps/apps/idpportal/public/assets/mcp-federation.svg` to `images/world-id-auth/mcp-federation.svg`. Edit the source illustration and regenerate when the diagram changes. + +Mintlify Autopilot may maintain other pages. It must not independently edit generated IDP pages. IDP content proposals belong in the canonical Markdown and then pass through this publisher. + +## How multirepo applies here + +Multirepo publishing lets a product team keep its documentation beside its code while a separate repository publishes the combined documentation site. The source files remain owned by the product repository. The publishing repository receives generated copies. + +Mintlify's [official multirepo action](https://github.com/mintlify/multirepo-action) copies child documentation folders and merges their `docs.json` navigation into a parent configuration. It can propose those updates through a PR. + +This IDP publisher uses the same one-way model with a focused workflow instead of that action. The existing sources are shared with Auth MCP and need issuer substitution, renamed destination routes, MDX escaping, and an approved public user-guide introduction. The focused publisher preserves the existing site navigation and avoids adding a Mintlify configuration to the canonical guide directory. No content needs to move out of `temporal-apps`. + +## Generate locally + +Check out `temporal-apps` beside `developer-docs`, and select the source commit you intend to publish. From the `developer-docs` root, run: + +```sh +node scripts/sync-world-id-auth.mjs \ + --source-dir ../temporal-apps \ + --source-commit <40-character-source-commit> +``` + +Pass the full commit of the source checkout. Review the generated pages and source lock together. To compare committed output with that source without writing files, add `--check`: + +```sh +node scripts/sync-world-id-auth.mjs \ + --source-dir ../temporal-apps \ + --source-commit <40-character-source-commit> \ + --check +``` + +The initial generation is pinned to `9eedf2dee3a8fb1d864e02c59a98982a2c811e61`, the source already reviewed for this migration. Future publishing PRs record their own source commit in the lock and PR description. + +## Configure automation + +The repository owner must install a scoped GitHub App with access to `worldcoin/temporal-apps` and `worldcoin/developer-docs`. It needs source contents read access, destination contents write access, and destination pull-request write access. Scope its installation to the required repositories. + +Configure these values in `developer-docs`: + +| Setting | Value | +| --- | --- | +| Actions variable `IDP_DOCS_APP_ID` | GitHub App ID | +| Actions secret `IDP_DOCS_APP_PRIVATE_KEY` | GitHub App private key | + +Merge the workflow to `main` to enable its hourly schedule. Manual dispatch accepts `source_ref`, defaulting to `main`, and resolves it to a source commit. The workflow checks out that source, generates the pages, and opens or updates the PR branch `codex/sync-world-id-auth`. A run with no generated changes should produce no publishing PR update. + +Until the workflow is merged, credentials are configured, and a run succeeds, automatic IDP synchronization is not active. Creating these files does not install the App, grant repository access, or configure secrets. + +## Review and publication + +Review each publishing PR against its linked source commit. Check content changes, release status, environment values, and any converter or manifest changes. A source merge alone does not prove that the described behavior is deployed. Hold or correct documentation that would present unreleased behavior as available. + +Merge the publishing PR through the normal documentation review process. The workflow never merges automatically. Mintlify publication follows the site's existing deployment configuration after merge. + +This publisher does not modify `temporal-apps`, deploy Auth or MCP services, install legacy redirects, or retire existing Auth documentation. Keep those cutover changes in their owning deployments. + +If a source change cannot be represented safely, correct the canonical Markdown or converter and rerun. Do not repair generated pages by hand or configure Autopilot to maintain a second version of their content. diff --git a/.mintlify/world-id-auth-source.json b/.mintlify/world-id-auth-source.json new file mode 100644 index 0000000..eeab1f6 --- /dev/null +++ b/.mintlify/world-id-auth-source.json @@ -0,0 +1,34 @@ +{ + "repository": "worldcoin/temporal-apps", + "sourceDirectory": "apps/idpapi/mcp/guides", + "configSha256": "71269061412f47a44c05097e62d21a49da47ebd35f1598c30bb4aee51d15fe03", + "sources": { + "apps/idpapi/mcp/guides/getting-started.md": "862ca6a6480fc8913321c46f4917131d48cc0f580cc3519568942315679912b2", + "apps/idpapi/mcp/guides/app-registration.md": "673e024239af407990e6f506307ae0d99b17ea8b89bf77ceb885e591585ac950", + "apps/idpapi/mcp/guides/oidc.md": "d857a800225e5c8314a5be36572c70c33fdbf24bb93bbaede2771d5a5a8a0e1c", + "apps/idpapi/mcp/guides/federation.md": "b6fcd29bad8ee213172b7648dbc0de87e807a4aa179dc3045838346aa5d58484", + "apps/idpapi/mcp/guides/id-token.md": "b65dc36cccecb1c1c71c4569eb413c70fb2e310e22382dc15fa3f56d3027e170", + "apps/idpapi/mcp/guides/step-up.md": "296958836f9ee967eab43a89c6d4d832338510a2eca78aaaf229424a1784c8f3", + "apps/idpapi/mcp/guides/human-in-the-loop.md": "1f84197deb081a6f22cda2e18bb34f959ff141825d711443f91cfcc57047a089", + "apps/idpapi/mcp/guides/xaa.md": "86e2567e430541a8d5237b7dad38eb84c1f972df692db83a5f90380a2869ed39", + "apps/idpapi/mcp/guides/world-id-users.md": "f2da89302b0cfcead0c8a20fef15a9a1477b45439b21660fc96f01b59eb7ed04", + "apps/idpapi/mcp/guides/coding-agent-setup.md": "7c2b4d14807b9b4fd64aec0dc672ce525f1da0bfb3c1ae582650996555ac6883", + "apps/idpapi/mcp/guides/testing.md": "b2c969f812a09d8951bc4178fcc92539580cfd9b926d13e17bad1c8c01a91adc", + "apps/idpportal/public/assets/mcp-federation.svg": "824e2df51c3333b8142fa72d6752340b1a5668c03bd3ebca3d3b2401d05257bc" + }, + "outputs": { + "world-id-auth/quickstart.mdx": "7e8dc53230bbed1279745f7f1e7718239823832ced3502ce3ff1a19c5ea67343", + "world-id-auth/app-registration.mdx": "bbdc9cdef72e0a657d95d6b8fbb9f0d1652c80de604496af4a597b019a4d4edc", + "world-id-auth/connect-your-backend.mdx": "7dd257bc70cdfe8a48a77595c2a26fd09ce0a51e39641077c980b8f7562b32fd", + "world-id-auth/connect-your-auth-server.mdx": "b17705ee9c7924d4b8129a72e0814e51086f28c881a899860135aa04287a1a95", + "world-id-auth/id-tokens-and-identity.mdx": "8a658155795a9a777feec44e7f889c6726395ec22a35f761c515814316394673", + "world-id-auth/step-up.mdx": "a6e7b90035b23ff8c146cae91b200c23e2f348b5f3ce26364683369d003a5e28", + "world-id-auth/human-in-the-loop.mdx": "65f6e7b7199d7f402052f19f5ca6aa7742f42ec3fa9f01bd222906228b06b9b2", + "world-id-auth/xaa.mdx": "f5edc53cc6d7adeae7aed05b28edaf19f6604a2cf4ebf1765acc2cc5a35551ad", + "world-id-auth/use-world-id-with-your-agent.mdx": "0853e0d8429e40fe783ce5ea596bedd86fd26fb8c9468cace0b18465dc3873ab", + "world-id-auth/build-with-a-coding-agent.mdx": "2cecc36b3ecca422a52b8a3ad7ab9b8b49e98134b9e8c0b11d024e577c9d71fe", + "world-id-auth/testing.mdx": "a08ed44d703fd918c19174fc8736d1d86433297d74430f73528ec35faa82883e", + "images/world-id-auth/mcp-federation.svg": "824e2df51c3333b8142fa72d6752340b1a5668c03bd3ebca3d3b2401d05257bc" + }, + "sourceCommit": "9eedf2dee3a8fb1d864e02c59a98982a2c811e61" +} diff --git a/README.md b/README.md index d3ab4e9..e3c9634 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Source for the official World developer documentation, published at **[docs.worl ## What's documented here - **World ID** (`world-id/`) — IDKit SDKs (JS, React, Swift, Kotlin, Go), Credentials, Selfie Check, on-chain verification, API reference. +- **IDP** (`world-id-auth/`) — OIDC integration, identity, step-up authentication, agent delegation, user connections, and developer MCP tools. See the [source mapping and publishing contract](.mintlify/world-id-auth-migration.md). - **Mini Apps** (`mini-apps/`) — building applications that run inside World App, including commands, sharing, growth, and the MiniKit reference. - **World Chain** (`world-chain/`) — quick start, provider integrations, tokens, and developer reference for the chain. - **AgentKit and agents** (`agents/`) — distinguishing human-backed agents from bots, plus Hats and human-in-the-loop flows. diff --git a/cspell.json b/cspell.json index 61daf8c..9fdf8eb 100644 --- a/cspell.json +++ b/cspell.json @@ -27,6 +27,7 @@ "CCIP", "Chainlink", "Cheatsheet", + "CIMD", "Completionist", "contractname", "crosschain", @@ -47,6 +48,7 @@ "ethersproject", "fdir", "fintech", + "frontmatter", "gatekept", "getenv", "gigagas", @@ -58,6 +60,10 @@ "Hyperlane", "idkit", "IDKit", + "idpapi", + "idpportal", + "idpweb", + "idpworker", "IERC", "inlines", "ifying", @@ -110,6 +116,7 @@ "Personalisation", "personhood", "Pimlico", + "PKCE", "pragma", "precheck", "predeploy", @@ -121,6 +128,7 @@ "quicknode", "quotidiennes", "Rabby", + "reauthentication", "readdirp", "Recompensas", "Récompenses", @@ -164,8 +172,11 @@ "tunnelmole", "tute", "uniffi", + "unconfigured", "unlinkable", "unpackedProof", + "unrun", + "unvalidated", "urlencode", "userop", "UUPS", @@ -183,6 +194,7 @@ "worldcoin", "worlddevelopersupport", "worldid", + "worldidp", "Worldscan", "XVCJ", "Zerion", diff --git a/docs.json b/docs.json index a8966f5..3a80697 100644 --- a/docs.json +++ b/docs.json @@ -126,6 +126,42 @@ } ] }, + { + "tab": "IDP", + "groups": [ + { + "group": "Get started", + "pages": [ + "world-id-auth/quickstart", + "world-id-auth/app-registration" + ] + }, + { + "group": "Authentication", + "pages": [ + "world-id-auth/connect-your-backend", + "world-id-auth/connect-your-auth-server", + "world-id-auth/id-tokens-and-identity", + "world-id-auth/step-up" + ] + }, + { + "group": "Agents", + "pages": [ + "world-id-auth/human-in-the-loop", + "world-id-auth/xaa", + "world-id-auth/use-world-id-with-your-agent" + ] + }, + { + "group": "Development", + "pages": [ + "world-id-auth/build-with-a-coding-agent", + "world-id-auth/testing" + ] + } + ] + }, { "tab": "Mini Apps", "groups": [ diff --git a/images/world-id-auth/mcp-federation.svg b/images/world-id-auth/mcp-federation.svg new file mode 100644 index 0000000..f2d8df0 --- /dev/null +++ b/images/world-id-auth/mcp-federation.svg @@ -0,0 +1,73 @@ + + Token handoff for your MCP server + + The World ID issuer sends an ID token to your auth server. Your auth server validates it, maps the subject to your + application's account, and issues an access token for your MCP to the MCP client. The client sends that access token + to your MCP server, which validates it before running a tool. Browser redirects and code exchanges are omitted. + + + + + + + + + + + + Token handoff + After browser sign-in and code exchange + + + World ID issuer + Authenticates the user + + + ID token + Subject and authentication claims + + + Your auth server + Maps the validated World ID subject + to your application's account + + + Access token for your MCP + Issued by your auth server + + + MCP client + Connects the agent to your server + + + MCP request + access token + The token from your auth server + + + Your MCP server + Validates access before running tools + + diff --git a/scripts/sync-world-id-auth.mjs b/scripts/sync-world-id-auth.mjs new file mode 100644 index 0000000..b0315a2 --- /dev/null +++ b/scripts/sync-world-id-auth.mjs @@ -0,0 +1,204 @@ +#!/usr/bin/env node +// Read canonical Markdown only. Never execute code from the source repository. +import { createHash } from "node:crypto"; +import { lstat, mkdir, readFile, realpath, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const repositoryRoot = fileURLToPath(new URL("../", import.meta.url)); +const manifestPath = new URL("../.mintlify/world-id-auth-pages.json", import.meta.url); +const lockPath = ".mintlify/world-id-auth-source.json"; +const publicIntroduction = "Your agent can read this guide because you authenticated with World ID and approved\nits OAuth access. This guide contains general guidance, not private account data."; +const publicReplacement = "When you authenticate with World ID and approve an agent's OAuth access, the agent\ncan read the authenticated version of this guide through the user MCP server.\nThis public guide contains the same general guidance, not private account data."; +const hash = (value) => createHash("sha256").update(value).digest("hex"); + +function validateConfig(config) { + if (config.repository !== "worldcoin/temporal-apps" || config.sourceDirectory !== "apps/idpapi/mcp/guides") { + throw new Error("Only the canonical temporal-apps guide directory can be published"); + } + if (config.issuer !== "https://auth.world.org" || !config.pages?.length) { + throw new Error("A production issuer and explicit guide allowlist are required"); + } + const sources = new Set(); + const destinations = new Set(); + for (const page of config.pages) { + if (!/^[a-z0-9-]+\.md$/.test(page.source) || !/^world-id-auth\/[a-z0-9-]+\.mdx$/.test(page.destination)) { + throw new Error("Guide paths must stay within the approved source and destination directories"); + } + if (sources.has(page.source) || destinations.has(page.destination) || !page.title || !page.description) { + throw new Error("Each guide needs a unique source, destination, title, and description"); + } + if ((page.source === "world-id-users.md") !== Boolean(page.publicUserGuide)) { + throw new Error("The authenticated user guide requires its explicitly approved public adaptation"); + } + sources.add(page.source); + destinations.add(page.destination); + } + for (const asset of config.assets ?? []) { + if (asset.source !== "apps/idpportal/public/assets/mcp-federation.svg" || asset.destination !== "images/world-id-auth/mcp-federation.svg") { + throw new Error("Only the explicitly approved federation diagram can be copied"); + } + } +} + +function rewriteLinks(markdown, config) { + const routes = new Map(config.pages.map((page) => [page.source.replace(/\.md$/, ""), `/${page.destination.replace(/\.mdx$/, "")}`])); + let result = markdown.replace(/(?:\{\{issuer\}\}|https:\/\/(?:sandbox\.)?auth\.world\.org)\/docs\/guides\/([a-z0-9-]+)(\.md)?/g, (_, id, extension = "") => { + if (!routes.has(id)) throw new Error(`Unmapped guide link: ${id}`); + return `https://docs.world.org${routes.get(id)}${extension}`; + }); + result = result.replace(/\]\((?:\.\/)?([a-z0-9-]+)\.md(#[^)\s]+)?\)/g, (_, id, fragment = "") => { + if (!routes.has(id)) throw new Error(`Unmapped relative guide link: ${id}`); + return `](${routes.get(id)}${fragment})`; + }); + // Markdown links use local routes; plain URLs and agent prompts stay absolute. + result = result.replace(/\]\(https:\/\/docs\.world\.org(\/world-id-auth\/[^)\s]+)\)/g, "]($1)"); + for (const asset of config.assets ?? []) result = result.replaceAll(asset.url, `/${asset.destination}`); + result = result.replaceAll("{{issuer}}", config.issuer); + if (/\{\{[^}\n]+\}\}/.test(result)) throw new Error("Unknown source placeholder requires an explicit publishing rule"); + return result; +} + +function escapeProse(text) { + return text.replace(/(`+)(.*?)\1|<((?:https?:\/\/|mailto:)[^>]+)>|[{}<>]/g, (match, ticks, _code, url) => { + if (ticks) return match; + if (url) return `[${url}](${url})`; + return { "{": "{", "}": "}", "<": "<", ">": ">" }[match]; + }); +} + +function headingSlug(heading) { + return heading.toLowerCase().replace(/[`*_]/g, "").replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-"); +} + +export function renderGuide(markdown, page, config) { + validateConfig(config); + let body = markdown.replace(/\r\n/g, "\n"); + if (!/^# [^\n]+\n/.test(body)) throw new Error(`Missing guide title: ${page.source}`); + body = body.replace(/^# [^\n]+\n+/, ""); + if (page.publicUserGuide) { + if (!body.startsWith(publicIntroduction)) throw new Error("The authenticated user introduction changed; review its public adaptation"); + body = publicReplacement + body.slice(publicIntroduction.length); + } + body = rewriteLinks(body, config); + const ids = new Map(); + let fence; + const lines = body.split("\n").map((line) => { + const marker = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/); + if (fence) { + if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) fence = undefined; + return line; + } + if (marker) { + fence = marker[1]; + return line.replace(/(`{3,}|~{3,})prompt\s*$/, "$1text"); + } + const heading = line.match(/^(#{2,6}) (.+?)(?: \{#([a-z0-9-]+)\})?$/); + if (!heading) return escapeProse(line); + const base = heading[3] ?? headingSlug(heading[2]); + const count = ids.get(base) ?? 0; + ids.set(base, count + 1); + const id = count ? `${base}-${count}` : base; + const aliases = (page.anchorAliases?.[id] ?? []).map((alias) => { + if (!/^[a-z0-9-]+$/.test(alias)) throw new Error(`Invalid heading alias: ${alias}`); + return `\n\n`; + }).join(""); + return `${aliases}${heading[1]} ${escapeProse(heading[2])} {#${id}}`; + }); + if (fence) throw new Error(`Unclosed code fence: ${page.source}`); + const metadata = [ + "---", + `title: ${JSON.stringify(page.title)}`, + `sidebarTitle: ${JSON.stringify(page.sidebarTitle ?? page.title)}`, + `description: ${JSON.stringify(page.description)}`, + '"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png"', + '"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png"', + "---", + "", + `{/* Generated from ${config.repository}/${config.sourceDirectory}/${page.source}. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */}`, + "", + ]; + if (page.environmentNote) metadata.push('import AuthEnvironment from "/snippets/world-id-auth-environment.mdx";', "", "", ""); + return `${metadata.join("\n")}\n${lines.join("\n").trimEnd()}\n`; +} + +async function readOptional(filename) { + try { return await readFile(filename, "utf8"); } + catch (error) { if (error.code === "ENOENT") return null; throw error; } +} + +async function readSource(sourceDir, relativePath) { + const filename = path.resolve(sourceDir, relativePath); + const resolved = await realpath(filename); + if (!resolved.startsWith(`${await realpath(sourceDir)}${path.sep}`) || !(await lstat(filename)).isFile()) { + throw new Error(`Source must be a regular file inside temporal-apps: ${relativePath}`); + } + return readFile(filename, "utf8"); +} + +export async function syncGuides({ sourceDir, sourceCommit, outputDir = repositoryRoot, check = false, config }) { + if (!/^[a-f0-9]{40}$/.test(sourceCommit ?? "")) throw new Error("Provide the full source commit SHA with --source-commit"); + config ??= JSON.parse(await readFile(manifestPath, "utf8")); + validateConfig(config); + const outputs = new Map(); + const sources = {}; + // Finish all reads and rendering before writing any generated file. + for (const page of config.pages) { + const source = `${config.sourceDirectory}/${page.source}`; + const markdown = await readSource(sourceDir, source); + sources[source] = hash(markdown); + outputs.set(page.destination, renderGuide(markdown, page, config)); + } + for (const asset of config.assets ?? []) { + const svg = await readSource(sourceDir, asset.source); + sources[asset.source] = hash(svg); + outputs.set(asset.destination, svg); + } + const previousText = await readOptional(path.join(outputDir, lockPath)); + const previous = previousText ? JSON.parse(previousText) : null; + const provenance = { + repository: config.repository, + sourceDirectory: config.sourceDirectory, + configSha256: hash(JSON.stringify(config)), + sources, + outputs: Object.fromEntries([...outputs].map(([filename, content]) => [filename, hash(content)])), + }; + const { sourceCommit: previousCommit, ...previousInputs } = previous ?? {}; + const sameInputs = JSON.stringify(previousInputs) === JSON.stringify(provenance); + const recordedCommit = sameInputs ? previousCommit : sourceCommit; + outputs.set(lockPath, `${JSON.stringify({ ...provenance, sourceCommit: recordedCommit }, null, 2)}\n`); + const changed = []; + for (const [filename, content] of outputs) { + if (await readOptional(path.join(outputDir, filename)) !== content) changed.push(filename); + } + if (!check) { + for (const filename of changed) { + const destination = path.join(outputDir, filename); + await mkdir(path.dirname(destination), { recursive: true }); + await writeFile(destination, outputs.get(filename)); + } + } + return { changed, sourceCommit: recordedCommit }; +} + +async function main() { + const args = process.argv.slice(2); + const options = {}; + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + if (argument === "--check") options.check = true; + else if (["--source-dir", "--source-commit", "--output-dir"].includes(argument) && args[index + 1] && !args[index + 1].startsWith("--")) { + const key = { "--source-dir": "sourceDir", "--source-commit": "sourceCommit", "--output-dir": "outputDir" }[argument]; + options[key] = path.resolve(args[++index]); + if (key === "sourceCommit") options[key] = args[index]; + } else throw new Error(`Unknown or incomplete argument: ${argument}`); + } + if (!options.sourceDir) throw new Error("Usage: node scripts/sync-world-id-auth.mjs --source-dir --source-commit [--check]"); + const result = await syncGuides(options); + if (options.check && result.changed.length) throw new Error(`Generated IDP files are out of date:\n${result.changed.join("\n")}`); + console.log(result.changed.length ? `Updated ${result.changed.length} IDP files from ${result.sourceCommit}` : "IDP files match the canonical Markdown"); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { + main().catch((error) => { console.error(error.message); process.exitCode = 1; }); +} diff --git a/scripts/sync-world-id-auth.test.mjs b/scripts/sync-world-id-auth.test.mjs new file mode 100644 index 0000000..09e6784 --- /dev/null +++ b/scripts/sync-world-id-auth.test.mjs @@ -0,0 +1,131 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { renderGuide, syncGuides } from "./sync-world-id-auth.mjs"; + +const page = { + source: "getting-started.md", + destination: "world-id-auth/quickstart.mdx", + title: "Quickstart", + description: "Connect your application.", +}; +const config = { + repository: "worldcoin/temporal-apps", + sourceDirectory: "apps/idpapi/mcp/guides", + issuer: "https://auth.world.org", + pages: [page], +}; +const sourceCommit = "a".repeat(40); + +async function fixture(t) { + const directory = await mkdtemp(path.join(tmpdir(), "idp-doc-sync-")); + t.after(() => rm(directory, { recursive: true, force: true })); + const sourceDir = path.join(directory, "source"); + const outputDir = path.join(directory, "output"); + await mkdir(path.join(sourceDir, config.sourceDirectory), { recursive: true }); + await mkdir(outputDir); + return { + sourceDir, + outputDir, + async write(name, content) { + await writeFile(path.join(sourceDir, config.sourceDirectory, name), content); + }, + }; +} + +test("preserves complete guide instructions and code while adding page metadata", () => { + const markdown = [ + "# Source title", + "", + "## Agent instructions", + "", + "Keep this and every later section.", + "", + "```json", + '{ "issuer": "{{issuer}}", "claim": "{subject}" }', + "```", + "", + "## Register an application", + "", + "Final registration instructions.", + ].join("\n"); + const rendered = renderGuide(markdown, page, config); + assert.match(rendered, /^---\ntitle: "Quickstart"/); + assert.match(rendered, /## Agent instructions \{#agent-instructions\}/); + assert.match(rendered, /## Register an application/); + assert.match(rendered, /Final registration instructions\./); + assert.ok(rendered.includes('```json\n{ "issuer": "https://auth.world.org", "claim": "{subject}" }\n```')); +}); + +test("maps guide links and raw Markdown prompts without moving service endpoints", () => { + const markdown = [ + "# Guide", + "", + "[Start]({{issuer}}/docs/guides/getting-started#next)", + "Read {{issuer}}/docs/guides/getting-started.md with your agent.", + "Use `{{issuer}}/api/v1/token` and `{{issuer}}/mcp/developer`.", + ].join("\n"); + const rendered = renderGuide(markdown, page, config); + assert.ok(rendered.includes("[Start](/world-id-auth/quickstart#next)")); + assert.ok(rendered.includes("https://docs.world.org/world-id-auth/quickstart.md")); + assert.ok(rendered.includes("`https://auth.world.org/api/v1/token`")); + assert.ok(rendered.includes("`https://auth.world.org/mcp/developer`")); + assert.ok(!rendered.includes("/docs/guides/")); +}); + +test("escapes MDX expressions and angle text while leaving inline code literal", () => { + const rendered = renderGuide("# Guide\n\nA value is {claim} and . Use `{inline}`.\n", page, config); + assert.ok(/(?:\\\{|{|{)claim(?:\\\}|}|})/.test(rendered)); + assert.ok(rendered.includes("<subject>") || rendered.includes("\\")); + assert.ok(rendered.includes("`{inline}`")); +}); + +test("rejects unmapped guide links and unknown source placeholders", () => { + assert.throws(() => renderGuide("# Guide\n\n[Missing]({{issuer}}/docs/guides/not-approved)", page, config)); + assert.throws(() => renderGuide("# Guide\n\nUse {{unknown}}/api.", page, config)); +}); + +test("adapts only the approved public user introduction and preserves account authorization", () => { + const userPage = { ...page, source: "world-id-users.md", publicUserGuide: true }; + const markdown = [ + "# For World ID users", + "", + "Your agent can read this guide because you authenticated with World ID and approved", + "its OAuth access. This guide contains general guidance, not private account data.", + "", + "## What you authorized", + "", + "With the `world-id:read` scope on `{{issuer}}/mcp`, your agent can read account information.", + ].join("\n"); + const rendered = renderGuide(markdown, userPage, { ...config, pages: [userPage] }); + assert.ok(!rendered.includes("because you authenticated")); + assert.ok(rendered.includes("`world-id:read`")); + assert.ok(rendered.includes("`https://auth.world.org/mcp`")); + assert.ok(rendered.includes("not private account data")); +}); + +test("regenerates deterministically and ignores files outside the approved guide list", async (t) => { + const f = await fixture(t); + await f.write(page.source, "# Guide\n\n## Agent instructions\n\nKeep the complete guide.\n"); + await f.write("unapproved.md", "# Do not publish\n"); + const options = { ...f, sourceCommit, config }; + const first = await syncGuides(options); + assert.ok(first.changed.includes(page.destination)); + const provenancePath = path.join(f.outputDir, ".mintlify/world-id-auth-source.json"); + const provenance = await readFile(provenancePath, "utf8"); + const second = await syncGuides({ ...options, sourceCommit: "b".repeat(40) }); + assert.deepEqual(second.changed, []); + assert.equal(await readFile(provenancePath, "utf8"), provenance); + assert.deepEqual(await readdir(path.join(f.outputDir, "world-id-auth")), ["quickstart.mdx"]); +}); + +test("a missing approved source fails before any destination is written", async (t) => { + const f = await fixture(t); + await f.write(page.source, "# Guide\n\nExisting source.\n"); + const missing = { ...page, source: "missing.md", destination: "world-id-auth/missing.mdx" }; + await assert.rejects(() => syncGuides({ ...f, sourceCommit, config: { ...config, pages: [page, missing] } })); + assert.deepEqual(await readdir(f.outputDir), []); +}); diff --git a/snippets/world-id-auth-environment.mdx b/snippets/world-id-auth-environment.mdx new file mode 100644 index 0000000..be36248 --- /dev/null +++ b/snippets/world-id-auth-environment.mdx @@ -0,0 +1,7 @@ + + Unless a section explicitly says Sandbox, examples use the production issuer + `https://auth.world.org`. For sandbox testing, use `https://sandbox.auth.world.org` + for discovery, portal registration, credentials, and all authentication endpoints. + Keep every part of an integration in the same environment. See + [Test your integration](/world-id-auth/testing#test-in-the-sandbox). + diff --git a/tailwind.css b/tailwind.css index ba391c7..9381276 100644 --- a/tailwind.css +++ b/tailwind.css @@ -157,13 +157,14 @@ } /* ------------------------------------------------------------------ */ - /* Floating pill header (Docs 3.0, desktop ≥1440px) — collapse the */ + /* Floating pill header (Docs 3.0, desktop ≥1600px) — collapse the */ /* theme's two-row header into the design's single 84px rounded bar. */ /* The theme's row scaffolding is flattened with display:contents so */ /* the logo, the tab row, and the search/assistant cluster lay out on */ /* ONE flex line — no absolute positioning, so the variable-width */ /* hosted cluster (Search + Ask Assistant) can never overlap the tabs. */ - /* Below 1440px — including when the hosted assistant panel narrows */ + /* Leave room for all tabs plus Search and Ask Assistant. */ + /* Below 1600px — including when the hosted assistant panel narrows */ /* the page without changing the window width — the two-row layout */ /* above remains: the pill activates on the header's own width via a */ /* container query, not a viewport media query. */ @@ -175,12 +176,12 @@ container-type: inline-size; } /* #navbar can't query its own width, so its parent — which the navbar - fills edge to edge, so both cross 1440px together — is the query + fills edge to edge, so both cross 1600px together — is the query container for the one rule below that styles #navbar itself. */ div:has(> #navbar) { container-type: inline-size; } - @container (min-width: 1440px) { + @container (min-width: 1600px) { /* No full-width backdrop: the pill floats on the page background. */ #navbar #navbar-transition { display: none; diff --git a/world-id-auth/app-registration.mdx b/world-id-auth/app-registration.mdx new file mode 100644 index 0000000..8598739 --- /dev/null +++ b/world-id-auth/app-registration.mdx @@ -0,0 +1,121 @@ +--- +title: "Register your application" +sidebarTitle: "Register your application" +description: "Register and manage your World ID OIDC application, credentials, callbacks, and benefit listing." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/app-registration.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +Sign in to the [developer portal](https://auth.world.org/portal) with Google to register an +OIDC client for your application or auth server. This registration supports +step-up, human-in-the-loop workflows, and primary sign-in. + +Check that your Google account is eligible in the target environment before +relying on self-service registration. Registration may be disabled, or you may +have reached the limit on clients you can own. If you receive +`account_not_eligible`, you may need help from the environment's operator. + +## Register your application {#register-your-application} + +1. **Register your callback URL.** Use HTTPS callback URLs. + Callback hosts must be ASCII; use Punycode for an internationalized domain. + Match the scheme, path, query, and port exactly; wildcards and + variable ports are not supported. Device-only clients also need a registered + redirect URI, although the device grant does not use it. +2. **Choose client authentication.** Register `client_secret_basic`, + `client_secret_post`, or `private_key_jwt`. You cannot change the method + after registration; switching methods requires a new client registration. + You can still rotate the existing client's secret or signing keys. + For `private_key_jwt`, generate the key pair in your backend's secure + environment and register only its public JWKS. +3. **Save the credentials in your backend.** Save the client ID and, when using + a shared secret, the secret shown once by the portal. Never ship a shared + secret or private key in frontend code or a CLI distributed to users. + Client secrets start with `worldidp_client_secret_`, so secret scanners can + flag leaked ones. + +World ID authentication with `openid` requires a client registered through the portal. +[XAA hosts](/world-id-auth/xaa) may instead use public or confidential CIMD +registration. OIDC dynamic client registration (DCR) is not supported. + +Developers sign in to the portal with Google to manage clients. Your users +authenticate with World ID in your application. + +Once registered, [connect your backend](/world-id-auth/connect-your-backend) or +[configure your auth server](/world-id-auth/connect-your-auth-server). +You can also [set a default verification preference](/world-id-auth/step-up#default-verification-preference) +for requests that omit both `acr_values` and an `acr` claim request. + +## Manage credentials and callbacks {#manage-credentials-and-callbacks} + +When rotating credentials, keep the old secret or key active until the new one works; +changes can take a short time to propagate. + +New portal applications receive a unique, permanent identity sector by default. +Callbacks can use different hostnames without a hosted sector document. +Applications in different sectors receive different pairwise `sub` values and +relay aliases, even when their callbacks use the same hostname. Existing +applications keep their current sectors and account identifiers. + +To share identifiers, enable identity sharing in a managed-sector application's +settings, then select it when registering another application. You must own every +application in that sector; being a member is not enough. Shared-sector +applications cannot transfer ownership individually. Deleting an application +does not make its sector available for reuse. Disabling sharing prevents new +applications from joining through that source; existing applications and +credentials remain unchanged. + +New portal registrations cannot choose a host-based sector. Hosted sector documents +are no longer fetched or editable. Existing host-based applications keep their +sectors and registered callbacks. A new client +cannot join a legacy host-based sector. Keep using the existing client to preserve those identifiers, or link +accounts explicitly when moving to a new managed sector. + +- **Change callbacks:** register each new exact URI before using it. Managed + sectors stay unchanged across hostname changes. For an existing host-based + client, only newly added callbacks must use the stored sector hostname; existing + callbacks can be retained, reordered, or removed. + Hostname comparison ignores case and a trailing dot; subdomains are distinct. +- **Move to another sector:** create a separate client. An existing client's + sector cannot change, and deleting and recreating a client does not migrate + your application's account mappings or permissions. + +Rotating credentials does not change subjects. Registering a new application +changes subjects unless you explicitly share its sector. Changing the issuer +changes the identity namespace. + +When migrating to a new sector, retain the old account mapping during the transition. +Require an authenticated session for the existing application account and a fresh, +browser-bound World ID linking attempt for the new client. Confirm the intended +account with the user, then store the new validated `(iss, sub)` only after +linking succeeds. Never guess which subjects correspond across sectors or link +an account using an unvalidated subject. + +## Get help from a coding agent {#get-help-from-a-coding-agent} + +Your coding agent can [read the public integration guides](/world-id-auth/build-with-a-coding-agent#give-your-coding-agent-the-guide) +without a plugin or MCP connection. Optionally +[connect developer MCP tools](/world-id-auth/build-with-a-coding-agent#connect-your-coding-agent) +to register and manage the application. Portal tools use Google authorization, and registration +and credential changes require your approval in the portal. Keep client secrets +and private keys outside agent conversations. The +[agent instructions](/world-id-auth/build-with-a-coding-agent.md) cover portal tool use. + +## Publish a benefit listing {#publish-a-benefit-listing} + +If your application offers a World ID benefit, save a Catalog listing for its OIDC client +in the developer portal: name, description, and HTTPS application URL. Optionally +fill in **Instructions** (`instructions`) with steps +to claim the benefit, a plugin link, or an MCP URL. Include any prerequisites and +how to recognize success. These instructions are public; +do not include credentials or tokens. The owner submits the listing for review. Only an approved, published +listing for an active client appears in `get_benefits`; registration and successful +authentication do not publish it. Changes to instructions require a new review. +Publication changes can take up to a minute to appear. + +Agents use the instructions to understand the partner's preferred claiming +flow, or start at the app URL if no instructions are supplied. Your application +enforces eligibility, authorization, and redemption. The application URL is +separate from the OIDC callback. diff --git a/world-id-auth/build-with-a-coding-agent.mdx b/world-id-auth/build-with-a-coding-agent.mdx new file mode 100644 index 0000000..f3bafec --- /dev/null +++ b/world-id-auth/build-with-a-coding-agent.mdx @@ -0,0 +1,232 @@ +--- +title: "Build with a coding agent" +sidebarTitle: "Build with a coding agent" +description: "Give your coding agent complete integration instructions and connect developer MCP tools." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/coding-agent-setup.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +Connect a coding agent to the World ID developer tools. No plugin is required. +The public integration guides are available without sign-in or an MCP connection. +Add the developer MCP server only when you want the agent to register or manage +your application through tools. + +## Give your coding agent the guide {#give-your-coding-agent-the-guide} + +Copy this prompt into your coding agent: + +```text +Read https://docs.world.org/world-id-auth/build-with-a-coding-agent.md and follow its instructions to integrate World ID into this project. +``` + +## Connect your coding agent {#connect-your-coding-agent} + +For optional app management through tools, add `https://auth.world.org/mcp/developer` in +Codex, Claude Code, or another coding agent's MCP settings as a Streamable HTTP +server with OAuth. The consumer World ID plugin does not bundle this server. +Use a separate connection name for each environment: + +| Environment | Server name | MCP URL | +| ----------- | ---------------------------- | ---------------------------------------------- | +| Production | `world-id-developer` | `https://auth.world.org/mcp/developer` | +| Sandbox | `world-id-developer-sandbox` | `https://sandbox.auth.world.org/mcp/developer` | + +The coding agent must support Client ID Metadata Documents (CIMD): it publishes +and supplies its own metadata URL. Connecting the coding agent requires no portal +OIDC registration or client secret. Dynamic client registration (DCR) is not +implemented. If your host does not support this connection, use the public guides +and portal directly. + +Keep the connection, guides, app registration, and credentials in the same +production or sandbox environment. Before authorizing, confirm that the current +session lists the developer MCP server at `https://auth.world.org/mcp/developer`. +Call `list_idp_guides` to check the connection; it requires no authorization. + +The commands below use this page's environment. For sandbox, replace +`world-id-developer` with `world-id-developer-sandbox` in every command. When +switching environments, use both the matching name and URL from the table. + +### Codex CLI {#codex-cli} + +Run `codex mcp list` in the environment where your coding session runs. If the +developer endpoint is already configured, use its existing name. Otherwise add it: + +```sh +codex mcp add world-id-developer --url https://auth.world.org/mcp/developer +``` + +For app management, authorize only the developer scope: + +```sh +codex mcp login world-id-developer --scopes developer-portal:manage +``` + +### Claude Code {#claude-code} + +Run `claude mcp list` to check for an existing connection. If it is missing, add it: + +```sh +claude mcp add --transport http --scope user world-id-developer https://auth.world.org/mcp/developer +``` + +For app management, start Claude Code and run `/mcp`. Select the configured +server and follow the browser sign-in flow. The `--scope user` option controls +where Claude Code stores the configuration; portal access uses the separate +OAuth permission `developer-portal:manage`. + +### Check the connection {#check-the-connection} + +Complete Google sign-in and consent in the browser when authorizing portal access. + +If the tools are unavailable, first check whether the developer server is listed +and enabled in the active host. Add it only if it is missing; otherwise use the +host's refresh controls. Start a new coding session if the host needs one to load +new tools. A desktop or workspace host manages its own connection; configuring a +separate CLI does not establish that the active chat can use it. + +Ask the agent to list the World ID integration guides to confirm the connection. +Reading guides requires no authorization. For app management, ask the agent to +check developer access and follow the Google sign-in and portal consent prompt. + +## Agent instructions {#agent-instructions} + +Read [Start here](/world-id-auth/quickstart) and inspect the project's +existing authentication setup. Use the +[OIDC guide](/world-id-auth/connect-your-backend) to connect a backend directly, or the +[federation guide](/world-id-auth/connect-your-auth-server) for an existing auth server. +Follow the [testing guide](/world-id-auth/testing) to verify the integration. +Use this environment's issuer and portal throughout, and ask for missing deployment +URLs. Keep client secrets and private keys out of chat and frontend code. + +To verify the MCP connection, call `list_idp_guides`; it requires no authorization. +Call `get_idp_guide` with an ID from that list to read a guide. The same content is +available through `resources/read` at each guide's `worldid://guides/` URI. +These are MCP resource identifiers; browser readers use +[Integration guides](/world-id-auth/quickstart). +A missing tool alone does not prove that the server is unconfigured or authorization +expired. Check the host's connection status before changing its configuration. + +To register or manage an application, call `get_portal_account` to check developer +access, then follow the host's Google sign-in and portal consent prompt if needed. +After authorization, retry `get_portal_account` in the same session before +continuing. If tools or authorization remain unavailable, use the host's connection +status and refresh controls. Stop on cancelled or failed sign-in and explain the +failure; do not repeatedly restart authorization. Confirm a successful tool result +before reporting that the connection works. + +## Developer-portal permission {#developer-portal-permission} + +To register an application, call `request_oidc_client_registration`. For other client +management tasks, call the appropriate portal tool. The MCP client follows the +server's OAuth challenge for +`developer-portal:manage`, obtains consent using Google portal sign-in, and +retries the call. Developer authorization requires no World ID verification. +World ID account authorization does not grant developer access. Request only +`developer-portal:manage` on `/mcp/developer`; do not combine it with `world-id:read`. +The server rejects mixed scopes. Account and benefit tools use a separate MCP +connection and plugin. If your coding agent cannot authorize developer access, +[register directly in the portal](https://auth.world.org/portal). +Keep browser cookies, tokens, client secrets, and private keys out of agent messages. + +Portal tools respect the user's memberships and owner permissions. Registration, +secret creation, secret revocation, and public JWKS replacement each create a +request for the user to approve in the portal. + +1. Supply a UUID `requestId`. Reuse it with identical inputs on retries. If a + response is lost, check `get_portal_credential_request` with that ID before retrying. +2. Show the user the returned `portalUrl` unchanged so they can review the request. + Do not approve it or retrieve secrets through browser automation. +3. Poll `get_portal_credential_request` sparingly with bounded requests. Continue only + while `status` is `pending` and `expiresAt` is in the future. +4. Stop on `completed`, `denied`, `expired`, or `revoked`, or when the deadline + passes. Surface tool errors and terminal failures; do not silently start a new + request. Only `completed` confirms success. + +Approval requests last 20 minutes. The API reports `expired` or `revoked` for +pending requests whose deadline has passed or whose authorizing grant is no +longer valid. +Secrets appear once to the user in the portal, never in MCP results. If the +approval response containing a new secret is lost, create another secret in +the portal; rereading the completed request cannot recover it. + +Before configuring the application, check that the completed registration's +`request.result.client` matches the intended application. It includes the public client +ID, callbacks, sector, and authentication method. + +Follow [Connect your backend to the World ID issuer (OIDC)](/world-id-auth/connect-your-backend) +to use the approved client. + +## Register an application with your agent {#register-an-application-with-your-agent} + +Registration requires a confidential backend that keeps credentials out of frontend +code and distributed CLIs. Collect the app name, callback URLs, authentication +method, and any optional public logo URL before requesting registration. +Use the project's existing configuration and ask for missing deployment URLs +rather than inventing them. + +- **Callbacks:** use exact HTTPS URLs, including the path, query, and port. + HTTP localhost, wildcard callbacks, and variable ports are not supported. + Use Punycode for internationalized callback hostnames. A device-only client + still needs a registered redirect URI. +- **Client authentication:** use `client_secret_basic` when the developer has + no preference and their backend supports it. `client_secret_post` and + `private_key_jwt` are also supported. For `private_key_jwt`, generate the key + pair in the backend's secure environment and register only the public JWKS. +- **Sector:** new clients receive a unique provider-managed sector by default; + callbacks may use different hostnames. To share identifiers, use an + owner-enabled `sectorSourceClientId` and confirm the developer owns every + client in that sector. Membership alone is insufficient. Hosted sector + documents are not supported. The sector and authentication method cannot change + after registration. See [Manage credentials and callbacks](/world-id-auth/app-registration#manage-credentials-and-callbacks). + +1. Call `list_oidc_clients` before creating a client. Reuse the intended app's + `clientId` if it already exists; ask which app when the match is ambiguous. +2. Confirm the registration details above with the developer. For protocol details, + see the [client-authentication reference](/world-id-auth/connect-your-backend#client-authentication). +3. Call `request_oidc_client_registration` with a fresh UUID `requestId`, `name`, + `redirectUris`, and `tokenEndpointAuthMethod`. Include `logoUri`, an approved + `sectorSourceClientId`, or public `jwks` when needed. Never send private keys. +4. Follow the **Developer-portal permission** workflow above. + The app does not exist until approval. The developer saves any generated client + secret directly from the portal into backend configuration. For `private_key_jwt`, + no secret is generated; the developer retains the private key that matches the + registered public JWKS. Never ask them to paste a secret or private key into chat. +5. After completion, return the public client ID, issuer `https://auth.world.org`, registered + callbacks, authentication method, and portal link using the approved + `request.result.client`. + +## Configure an existing application with your agent {#configure-an-existing-application-with-your-agent} + +Read `get_oidc_client` before updating. `update_oidc_client` replaces the +configuration: send `clientId`, `name`, `redirectUris`, `status`, and `logoUri`, +preserving values the developer did not ask to change. +Use `null` to preserve the logo, or an empty string only when asked to remove it. +Omit `sectorIdentifierUri` or echo its existing value; historical document URIs +cannot be cleared or replaced. Existing host-based callbacks can be retained, +reordered, or removed; only newly added callbacks must use the stored sector +hostname. Managed clients can change callback hostnames freely. +Preserve the immutable sector and authentication method. Report changes only +after the tool succeeds; reads may briefly lag successful changes. + +## Publish a benefit listing {#publish-a-benefit-listing} + +Registration does not publish a benefit. There is no catalog submission MCP tool. + +1. Direct the developer to `https://auth.world.org/portal/clients/{clientId}?tab=catalog`, + replacing `{clientId}` with the actual client ID, to open the app's **Catalog** tab. +2. Save a listing with a name, description, and HTTPS **Website URL** (`appUrl`). + This is where users claim the benefit; it is separate from the OIDC callback. +3. Optionally add **Instructions** (`instructions`) describing prerequisites, + human or browser steps, any partner plugin link or MCP URL, and how to confirm + success. These instructions become public after approval; never include + credentials or tokens. If omitted, agents start at the website URL. +4. The owner of the active app submits the saved listing for review. A separate + authorized reviewer who is not an app member must approve it. Approval publishes + the listing; changes to instructions require a new review. + +Only an approved, published listing for an active client appears in `get_benefits`. +Do not report a pending submission as approved or published. Publication changes +can take up to a minute to appear. The application enforces eligibility, +authorization, and redemption. diff --git a/world-id-auth/connect-your-auth-server.mdx b/world-id-auth/connect-your-auth-server.mdx new file mode 100644 index 0000000..3472477 --- /dev/null +++ b/world-id-auth/connect-your-auth-server.mdx @@ -0,0 +1,135 @@ +--- +title: "Connect your auth server" +sidebarTitle: "Connect your auth server" +description: "Use the World ID issuer as an upstream provider for your auth server or MCP server." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/federation.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +Use the World ID issuer as an upstream OpenID Connect provider when your +application already has an auth server, such as a login backend or OAuth +authorization server. Your auth server is the confidential OIDC client +registered with the World ID issuer. The issuer verifies the person's World ID proof; +your auth server controls your application's sessions, API tokens, permissions, +and consent. + +This integration uses the World ID issuer's OIDC endpoints. The issuer does not +configure your auth server, host its downstream OAuth endpoints, or implement +OpenID Federation trust-chain discovery. Verify that your auth server meets the +requirements below before selecting a generic “custom OIDC” connector. + +## Configure your auth server {#configure-your-auth-server} + +1. **[Register your auth server](/world-id-auth/app-registration)** in the + developer portal. Use the auth server's callback URL. Your application's + homepage or an MCP client's callback cannot substitute for it. Choose the + authentication method your auth server supports and keep its credentials + there. +2. **Set the issuer** to `https://auth.world.org` and the discovery URL to + `https://auth.world.org/.well-known/openid-configuration`. Use its authorization endpoint, + token endpoint, and JWKS URI. Do not select the `/mcp` OAuth issuer for + federation. +3. **Match the [OIDC settings](/world-id-auth/connect-your-backend#configure-your-oidc-library):** + authorization code, query response mode, `scope=openid` (optionally adding `email`), fresh `state` + and `nonce`, and S256 PKCE, including for confidential clients. Disable + connector defaults that add `profile` or `offline_access`. +4. **Validate the ID token** as described in + [Validate the ID token](/world-id-auth/connect-your-backend#validate-the-id-token). +5. **Read identity from the validated ID token.** Map `(iss, sub)` to your + application's account. Preserve validated `auth_time`, `acr`, and `amr` if you + will enforce step-up. [UserInfo and inbox email claims](/world-id-auth/id-tokens-and-identity) + are supported, but email is absent for accounts without an enabled inbox. + Configure your connector to handle that absence. +6. **Issue your own credentials.** Issue your application session or + resource-bound access token after your own authorization checks. The World ID + issuer does not issue an OIDC refresh token for authentication with `openid`. + Your auth server may maintain its own session and refresh policy; renewals + under that policy do not refresh World ID authentication. + +See [Client authentication](/world-id-auth/connect-your-backend#client-authentication) +for the credential formats. Do not assume every library's default OIDC +configuration satisfies this contract. + +Follow [Store and match sub](/world-id-auth/id-tokens-and-identity#store-and-match-sub) +for account linking and subject matching. Multiple applications using one auth +server may share the subject issued by the World ID issuer. Configure how your +auth server separates its downstream tenants and users. + +Before migrating an auth server or callback domain, follow +[Manage credentials and callbacks](/world-id-auth/app-registration#manage-credentials-and-callbacks). + +## Connect the World ID issuer to your MCP server {#connect-the-world-id-issuer-to-your-mcp-server} + +Connect the World ID issuer to the auth server used by your MCP server. Your auth +server validates the issuer's ID token and maps `(iss, sub)` to your application's +account. This lets you associate MCP activity with the same account across +sessions. Your auth server issues access tokens, and your MCP server controls +access to its tools. The MCP client is the software connecting the agent to your +server. + +![The World ID issuer returns an ID token to your auth server. Your auth server maps the subject to your application's account and issues an access token to the MCP client, which uses it to call your MCP server.](/images/world-id-auth/mcp-federation.svg) + +The diagram shows which service issues and receives each token. Browser +authorization uses two code exchanges: + +- **World ID issuer → your auth server:** the issuer redirects the user's browser + to your auth server's callback with a code. Your auth server exchanges it for + an ID token and validates the result. +- **Your auth server → the MCP client:** after checking permissions and required + consent, your auth server redirects the browser to the MCP client's callback + with a different code. The client exchanges it for an access token for your + MCP server. + +Your MCP server publishes protected-resource metadata identifying your auth +server and challenges requests without a valid access token. The MCP client uses +that metadata to start authorization in the user's browser. For tool calls, your +MCP server validates the access token's issuer, audience, expiry, permissions, +and revocation policy. + +Keep the two code exchanges' state, PKCE verifiers, callbacks, and codes separate. +Bind both exchanges to the same pending login. Use existing OAuth/MCP components +for client registration, redirect validation, S256 PKCE, resource binding, and +consent. + +Do not advertise `https://auth.world.org/mcp` as your MCP server's auth server: it issues +tokens only for the World ID user and developer MCP resources and rejects any +other `resource`. +It cannot mint access tokens for your service. Passing tokens from the World ID +issuer to your MCP server does not establish an authorized integration. + +An agent can alternatively use a backend that supports the OIDC device flow. +The backend obtains and validates an ID token, then issues its own credential for +the agent. Both the agent client and backend must support this flow; ordinary +MCP OAuth clients do not automatically use the OIDC device grant. + +## Require recent authentication through your auth server {#require-recent-authentication-through-your-auth-server} + +Your auth server may reuse its own session before redirecting to the World ID +issuer. For an operation that needs fresh authentication, make your auth server +start a new upstream OIDC request with `max_age=0` or `prompt=login`. Request an +Orb or Face Auth class explicitly when your policy requires World ID proof. Preserve +the validated result from the issuer and check its freshness when the operation +runs. Follow [Step-up through your auth server](/world-id-auth/step-up#through-your-auth-server) +for each layer's requirements. + +## Verify the integration {#verify-the-integration} + +Test these cases, inspecting the actual upstream authorization parameters and +validated claims without logging tokens or callback query strings: + +- Sign in with and without an enabled inbox, and match UserInfo `sub` to the ID token. +- Reuse an existing session and link an account. +- Reject reauthentication with the wrong account and require step-up when + authentication is too old. +- If you use MCP, reject a token intended for a different resource. + +A browser redirect back to the application is only one part of this test. + +Ending the World ID issuer's browser session or revoking its saved consent does +not automatically terminate your auth server's sessions or tokens. Provide your +own sign-out and revocation behavior; there is no advertised OIDC end-session or +back-channel logout endpoint to configure. + +Reference: [MCP authorization](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). diff --git a/world-id-auth/connect-your-backend.mdx b/world-id-auth/connect-your-backend.mdx new file mode 100644 index 0000000..31207ad --- /dev/null +++ b/world-id-auth/connect-your-backend.mdx @@ -0,0 +1,372 @@ +--- +title: "Connect your backend" +sidebarTitle: "Connect your backend" +description: "Connect your backend to the World ID issuer with OpenID Connect (OIDC)." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/oidc.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +Connect your backend to the World ID issuer with OpenID Connect (OIDC). Your backend receives +a signed ID token and validates it before using the account identifier and +authentication time. Step-up, human checkpoints, and primary sign-in all use this +connection. + +Your backend is a confidential OIDC client that keeps its +credentials private. Browser-only, native, and CLI applications also need a backend for +this integration. If your auth server already handles sign-in for your application, +follow [Connect the World ID issuer to your auth server](/world-id-auth/connect-your-auth-server) instead. + +Before you start, [register your application](/world-id-auth/app-registration) to +configure its callback URLs and client authentication and get its credentials. + +## Configure your OIDC library {#configure-your-oidc-library} + +| Setting | Value | +| --------------------- | ------------------------------------------------------------------------------------------------------ | +| Issuer | `https://auth.world.org` | +| Discovery | `https://auth.world.org/.well-known/openid-configuration` | +| Response type | `code`, with query response mode | +| Scope | `openid`, optionally with `email` | +| PKCE | `S256`, required | +| Client authentication | Your registered method; see [Client authentication](/world-id-auth/connect-your-backend#client-authentication) | + +Read the endpoints and signing keys (`jwks_uri`) from the discovery document. +Turn off library defaults that add `profile`, `offline_access`, +`prompt=consent`, or `prompt=select_account`. Use registration, discovery, and +credentials from the same environment. + + + +## Authenticate through the browser {#authenticate-through-the-browser} + +1. **Start authorization.** Redirect the browser to the discovered authorization + endpoint with `client_id`, the exact registered `redirect_uri`, + `response_type=code`, `scope=openid`, a fresh `state`, an optional fresh + `nonce`, and an S256 PKCE challenge. Keep the PKCE verifier in your server-side + state for this attempt. +2. **Handle the callback.** The World ID issuer handles passkey or World ID proof authentication and + browser-session eligibility, then redirects to your callback. Validate the + returned `state` and require the response's `iss` parameter to match the + discovered issuer exactly before redeeming the code. Success and error + responses both include `iss`. Treat an authorization error as a failure, + never as success. +3. **Exchange the code.** POST the code to the discovered token endpoint, + authenticated with your [registered method](/world-id-auth/connect-your-backend#client-authentication). + Each code can be used once and expires after five minutes. +4. **Validate the ID token** as described in + [Validate the ID token](/world-id-auth/connect-your-backend#validate-the-id-token), + then find the account by `(iss, sub)`. + +The authorization request has these decoded query parameters. URL-encode them +with your library and generate fresh random values for each attempt: + +```text +GET https://auth.world.org/api/v1/authorize +client_id= +redirect_uri=https://app.example/auth/world/callback +response_type=code +scope=openid +state= +nonce= +code_challenge= +code_challenge_method=S256 +``` + +The PKCE verifier is a cryptographically random value of 43–128 characters; keep +it private until the code exchange. The token request is a form body, not JSON. +For a client registered with `client_secret_post`, its decoded fields are: + +```text +POST https://auth.world.org/api/v1/token +Content-Type: application/x-www-form-urlencoded + +grant_type=authorization_code +client_id= +client_secret= +code= +redirect_uri=https://app.example/auth/world/callback +code_verifier= +``` + +For the portal's default `client_secret_basic`, remove the secret from the body +and send Basic authentication instead. For `private_key_jwt`, send the assertion +fields described under [Client authentication](/world-id-auth/connect-your-backend#client-authentication). +Never log request bodies, Authorization headers, or callback query strings. + +The response contains `id_token`, an opaque `access_token`, `token_type`, and +`expires_in`. See [Token response](/world-id-auth/id-tokens-and-identity#token-response) +for each field and [Decoded ID token](/world-id-auth/id-tokens-and-identity#decoded-id-token) +for an example payload. + +## Validate the ID token {#validate-the-id-token} + +Validate every ID token with an OIDC library before using its claims. Merely +decoding a JWT is not validation. Check: + +- **Issuer:** exactly `https://auth.world.org`. +- **Signature:** RS256 with a signing key from the discovery document's `jwks_uri`. + Cache the JWKS with bounded refresh and handle signing-key overlap. +- **Audience:** your client ID. +- **Expiration:** reject the token after `exp`. +- **Nonce:** matches the value you sent, if you sent one. +- **Authentication context:** when you require freshness, check `acr`, `amr`, and + `auth_time` as described in [Step-up authentication](/world-id-auth/step-up). + +After validation, use `(iss, sub)` as the World ID account identifier; see +[Store and match sub](/world-id-auth/id-tokens-and-identity#store-and-match-sub). +Use `auth_time`, not `iat`, to check when the user authenticated: an existing +browser session can produce a new token without a new World ID proof. Issuer +sessions expire after 180 days without authenticated browser activity; activity +renews that deadline, while `prompt=login` and `max_age` still require fresh +authentication when requested. + +## Client authentication {#client-authentication} + +Authenticate at the token and device authorization endpoints with the client's +registered method: + +| Method | How to send credentials | +| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `client_secret_basic` (portal default) | Form-encode the client ID and secret and send them in the Authorization header as Basic credentials. An optional body `client_id` must match. | +| `client_secret_post` | Send `client_id` and `client_secret` in the form body, without an Authorization header. | +| `private_key_jwt` | Send `client_id`, `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`, and `client_assertion` in the form body, without an Authorization header. | + +For `private_key_jwt`, register the public JWKS in the portal and keep the private +key in your backend. The assertion is a compact RS256 JWT: + +- `kid` selects a registered RSA key (2048–8192 bits). It may be omitted when + exactly one key is registered. +- `iss` and `sub` are both the client ID. +- The single `aud` is `https://auth.world.org` or the exact endpoint being called. +- Include `iat`, `exp` (at most one hour after `iat`), and a unique `jti` of at + most 256 bytes. Expiration is strict; `iat` and optional `nbf` allow 30 seconds + of clock skew. + +Use a fresh assertion for every request, retry, and device poll, including after +`authorization_pending`. Assertions are single-use across endpoints. A rejected +assertion returns HTTP 400 `invalid_client`. + + + +## Connect a headless agent (device flow) {#connect-a-headless-agent-device-flow} + +Use the device authorization grant when an agent or CLI cannot receive a browser +callback. Run it from your backend. The service running your agent can act as this +backend if it securely stores the client secret or private key and validates the +returned ID token. It authenticates to the World ID issuer as a +[confidential OAuth client](https://www.rfc-editor.org/rfc/rfc6749.html#section-2.1). +Keep client secrets, private keys, device codes, and tokens in server-side +credential storage, outside model messages. + +### Supported device profile {#supported-device-profile} + +Device authorization is available to active confidential clients regardless of +their sector. + +Configure the client for the device grant separately from browser authorization: + +| Input or result | Device grant contract | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Initiation | `scope=openid` for World ID authentication or `scope=openid offline_access` for XAA delegation, form-encoded, with confidential client authentication. | +| Polling | Form-encoded `grant_type=urn:ietf:params:oauth:grant-type:device_code`, `device_code`, and the same client's authentication. | +| Browser parameters | No callback, PKCE, or `state` exchange. `nonce`, `prompt`, and `max_age` do not provide request controls for this grant. Fresh authentication is required regardless. | +| Authentication class | Supports `acr_values` preferences and essential ID-token `acr` requests through `claims`; see [Face Auth](/world-id-auth/step-up#require-face-auth). | +| Unknown parameters | Ignored when supplied once. A successful initiation does not mean an extension was understood or enforced. | +| Malformed or repeated fields | Malformed form encoding and repeated fields, including unknown parameters and identical repeated values, return `invalid_request`. | +| ID token | Validate signature, issuer, audience, expiry, and required `auth_time`, `acr`, and `amr`. Device tokens omit `nonce`, even if one was sent; accept its absence for this profile. | +| `binding_message` or action terms | Unsupported. They are not displayed on the approval page or signed into the ID token. The human sees the requester and user code. | + +Remove unsupported device options from client configuration and diagnose them +locally; do not probe support by checking for HTTP success. Unknown parameters +remain compatible with extensions under +[RFC 8628 §3.1](https://www.rfc-editor.org/rfc/rfc8628.html#section-3.1). +Browser authorization still validates a nonce when one was requested. + +This profile returns authentication, not a signed action-approval receipt. If +your application requires such a receipt, reject its absence and leave the +operation unauthorized. A future receipt profile would need advertised support +and validation of its returned result; an ordinary ID token cannot satisfy that +requirement. Business-action display, consent, and authorization remain your +application's responsibility. + +### Device flow {#device-flow} + +1. **Start.** POST form-encoded `scope=openid` to the discovered + `device_authorization_endpoint` (`https://auth.world.org/api/v1/device_authorization`), + authenticated with your registered method. Bind this attempt to the requesting + agent's private session in your backend. +2. **Show the code.** The response contains `device_code`, `user_code`, + `verification_uri`, `verification_uri_complete`, `expires_in`, and `interval`. + Both `expires_in` and `interval` are measured in seconds. Show `user_code` and + a connection link using `verification_uri_complete`, or show `verification_uri` with instructions to + enter the code there. Never build the approval link yourself. See + [device flow user interaction](https://www.rfc-editor.org/rfc/rfc8628.html#section-3.3). +3. **Keep the device code private.** Your backend uses `device_code` with its + client credentials to retrieve tokens after approval. Keep it out of UI output, + logs, and model messages. +4. **Poll.** Wait at least `interval` seconds, then POST + `grant_type=urn:ietf:params:oauth:grant-type:device_code` and `device_code` to + the discovered token endpoint with the same client authentication. This grant + uses no callback, PKCE verifier, browser cookie, `state`, or `nonce`. Handle + each response as shown in the table below. +5. **The user approves.** The human checks the requester and user code, then + authenticates with an enrolled passkey or approves a proof request in the World ID app, + according to the requested authentication class. The issuer verifies the fresh assertion + or proof before approving this attempt. Denial ends the attempt. The device flow creates + no browser session, and an existing browser cookie cannot replace fresh authentication. +6. **Validate and bind.** Success returns the same token response as browser + authentication. [Validate the ID token](/world-id-auth/connect-your-backend#validate-the-id-token); + it has no `nonce`. Bind `(iss, sub)` atomically to the intended grant in your + backend, then give the agent client an access credential issued by your + backend. Confirm that an authenticated read of your backend resolves to that + grant. + +| Poll response | What to do | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| HTTP 400 `authorization_pending` | Keep waiting and poll again after the interval. | +| HTTP 400 `slow_down` | Add five seconds to all later intervals. An early poll can return this before the grant is checked; it never extends the attempt's expiry. | +| `access_denied`, `expired_token`, `invalid_grant`, other OAuth error | Stop polling. | +| HTTP 503 | Temporary unavailability, never approval. End this attempt. | +| HTTP 429 | Per-network-address flood protection on back-channel endpoints, separate from the per-client limits. It can end the attempt; `Retry-After` does not override the stop rules. | +| Connection timeout | Back off before polling again. | + +Set each request's timeout within the attempt's remaining lifetime, and stop +when the attempt expires. Do not automatically restart authorization in a loop +after a failed attempt. + +For `client_secret_post`, the decoded start form is: + +```text +POST https://auth.world.org/api/v1/device_authorization +Content-Type: application/x-www-form-urlencoded + +client_id= +client_secret= +scope=openid +``` + +After waiting the returned `interval`, the decoded poll form is: + +```text +POST https://auth.world.org/api/v1/token +Content-Type: application/x-www-form-urlencoded + +grant_type=urn:ietf:params:oauth:grant-type:device_code +client_id= +client_secret= +device_code= +``` + +Use your form encoder. For other methods, change the authentication fields or +header exactly as for the browser code exchange. + +### Expiry and rate limits {#expiry-and-rate-limits} + +- The device code expires 20 minutes after creation, even if approval occurs near + expiry, and can be redeemed once. See + [recovery after interrupted requests](/world-id-auth/connect-your-backend#recovery-after-interrupted-requests) + for when to retry or start again. +- If you cannot tell whether a start request succeeded, it may leave an unused + attempt until expiry. Do not retry it in a tight loop. +- Starts and polls share a per-client request limit, and starts also have a + per-client initiation limit. A limited start returns HTTP 429 with + `Retry-After: 60`; a limited poll returns HTTP 400 `slow_down`, so polling can + continue at the increased interval. +- Manual user-code lookup is rate-limited per network address and returns HTTP 429 + with `Retry-After` when limited. Complete approval links do not use that lookup. +- The code-entry page retries a not-found code once before showing an error. If a + newly issued code is still not found, wait briefly and try again. + +Your backend or agent client must support the device flow; OIDC or MCP library +support alone does not guarantee it. + +## Cross-App Access for agent hosts {#cross-app-access-for-agent-hosts} + +Agent hosts request `openid offline_access` for delegation controlled by the user. +The human selects reviewed apps; new apps require explicit selection. +Authorization expires after 180 days without a successful token refresh or +ID-JAG exchange. Either renews that inactivity deadline, but neither proves fresh +World ID presence. + +The host's authentication layer manages token exchange, refresh, and credentials. +Follow the [XAA guide](/world-id-auth/xaa) to configure an app's authorization +server, enroll the app, and understand user consent and revocation. +World ID authentication with `openid` remains unchanged. + +## Recovery after interrupted requests {#recovery-after-interrupted-requests} + +- **Code exchange returns HTTP 503 or times out:** You can retry a limited number + of times before the code's original expiry, using the same code, redirect URI, + and PKCE verifier. +- **Device poll fails:** Follow the + [polling and stop rules](/world-id-auth/connect-your-backend#device-flow). +- **Exchange returns `invalid_grant`:** Treat it as failure. + Start a new authentication attempt. Each code can be redeemed once. If a + successful exchange's response was lost and you retained no tokens, you must + also start again. + +For [`private_key_jwt`](/world-id-auth/connect-your-backend#client-authentication), +generate a fresh assertion for every retry or poll, even after a signing failure. +Keep codes, assertions, tokens, PKCE verifiers, and client credentials out of logs. + +Before retrying a payment or another external action, check your application's saved +status and use its idempotency controls. + +## Primary sign-in {#primary-sign-in} + +To use World ID as your application's primary or only sign-in, use the browser or device +flow in this guide. After validating the ID token, find or create your application's +account using `(iss, sub)` and issue your own session. See the +[claim reference](/world-id-auth/id-tokens-and-identity#claim-reference) for optional inbox email claims. + +You can then add [step-up authentication](/world-id-auth/step-up) for +sensitive actions or [human checkpoints](/world-id-auth/human-in-the-loop) +for agent workflows. Both use the same OIDC connection. + +## Limitations {#limitations} + +- **Unsupported grants and request features.** Implicit and hybrid grants, client + credentials, and request objects (`request` / `request_uri`) are not implemented. + Refresh tokens and token exchange are available only for the XAA profile above. +- **Device flow and action approval.** See the + [supported device profile](/world-id-auth/connect-your-backend#supported-device-profile) + for request controls, parameter validation, and action-approval limitations. +- **Access tokens authorize UserInfo only.** The token response's opaque + access token does not authorize calls to the World ID MCP server, your APIs, or + downstream MCP tools. Protected downstream MCP tools require their own + resource-bound access tokens and MCP OAuth integration, which device + authorization does not supply. The World ID MCP server uses separate, + renewable grants. + Relying services own their accounts, API credentials, grants, eligibility, + quotas, and redemption rules. +- **Delegation requires explicit XAA consent.** Consent for browser authentication + is not a downstream agent grant. Reuse valid credentials issued by your + application before starting another authentication attempt. +- **Issuer logout does not reach your application.** Ending the World ID issuer's + browser session does not revoke your application's sessions, tokens, grants, or benefits. + +## Troubleshooting {#troubleshooting} + +OAuth failures include a human-readable `error_description` alongside `error` in +the JSON response or authorization error redirect. Use the description to diagnose +malformed parameters, and use the `error` code to decide how your application responds. +Descriptions never echo submitted values. A client or callback mismatch returns +an error without redirecting to your callback, using the same guidance for either mismatch. +Credential, grant, and service errors use deliberately generic descriptions. +Keep dependency failures distinguishable from invalid authentication. + +| Symptom | Check | +| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `invalid_scope` | Request `openid` for World ID authentication or `openid offline_access` for XAA delegation when discovery advertises it; `email` is also accepted. Remove other library scopes. | +| HTTP 400 `unsupported_grant_type` | Use a grant type advertised by discovery. Refresh and ID-JAG exchange are unavailable while XAA is disabled. | +| Authorization `invalid_request` | Check the active client, exact registered callback, state, nonce, S256 challenge, response mode, and supported prompt. Bad client/callback requests fail without redirecting to your callback. | +| `invalid_client` at token/device endpoint | Check environment, registered auth method, credential encoding, and that methods are not mixed. For JWTs check audience, key, expiry, and a fresh JTI per request. Basic failures use HTTP 401; POST/JWT failures use HTTP 400. | +| `invalid_grant` | Check expiry, single-use code, exact callback, PKCE verifier, and client binding. Do not blindly replay a possibly consumed code. | +| `login_required` / `interaction_required` | A `prompt=none` request cannot complete silently. Start an interactive request when appropriate. | +| `account_not_eligible` / `client_registration_disabled` in portal | Check participant Google-account eligibility and registration availability with the organizer/operator. These are not World ID proof failures. | +| HTTP 503 | Treat as temporary unavailability, never invalid identity or successful approval. Use bounded requests; apply the device polling rules above. | + +References: [OIDC Core](https://openid.net/specs/openid-connect-core-1_0.html), +[device authorization](https://www.rfc-editor.org/rfc/rfc8628.html). diff --git a/world-id-auth/human-in-the-loop.mdx b/world-id-auth/human-in-the-loop.mdx new file mode 100644 index 0000000..ceaaab6 --- /dev/null +++ b/world-id-auth/human-in-the-loop.mdx @@ -0,0 +1,76 @@ +--- +title: "Human in the loop" +sidebarTitle: "Human in the loop" +description: "Pause an agent workflow and require fresh World ID authentication before continuing." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/human-in-the-loop.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +Pause an agent workflow and require fresh World ID authentication before it +continues. Your application stores the pending work, validates the authentication result, +and decides whether the workflow may resume. + +The World ID issuer does not receive, display, or approve the pending business action. If +your application also requires approval of that action, implement the approval UI and +record the decision in your application. + +## Require fresh authentication {#require-fresh-authentication} + +[Register your application](/world-id-auth/app-registration) and implement either +[direct OIDC](/world-id-auth/connect-your-backend) or +[federation through your auth server](/world-id-auth/connect-your-auth-server). +This works whether your application uses World ID alongside an existing sign-in or as its +primary sign-in. When adding World ID to an existing account, explicitly link it +while the user is signed in to that account. Complete this link before relying +on World ID for later checkpoints. + +For a browser flow, follow the +[step-up procedure](/world-id-auth/step-up#implement-step-up). Use +`max_age=0` or `prompt=login` to require a new World proof for each attempt. A +positive `max_age`, such as `300`, permits recent authentication to be reused and +does not require a new interaction for every action. + +If you use an auth server, ensure it +[passes the freshness requirement to the World ID issuer and preserves the validated result](/world-id-auth/step-up#through-your-auth-server). + +## Optional: collect action approval in your application {#optional-collect-action-approval-in-your-application} + +Your application implements this approval flow. For example, it may require the user to +review a draft before an agent publishes it: + +1. **Store the pending action.** Your backend stores the pending publication, + exact draft version, destination, owning account, and expiry, and binds it to + the authentication attempt as in step-up. Keep publication blocked. +2. **Authenticate.** Request `max_age=0` or `prompt=login` and validate the result + [as in step-up](/world-id-auth/step-up#implement-step-up): the returned + `(iss, sub)` must match the account awaiting approval. Check `auth_time` for + the required freshness and `acr` and `amr` for the required assurance. +3. **Ask for approval.** Show the authenticated user the stored draft and + destination in your application, and ask them to approve or deny publication. Record + approval against that account and exact action. The World ID issuer provides the + authentication result; your application captures approval of what will be published. +4. **Publish once.** Recheck permissions, freshness, expiry, and approval before + publishing only the approved version. Prevent retries from publishing twice. A + changed draft or destination requires new approval. Denial, cancellation, + failed validation, or expiry leaves publication blocked. + +With `max_age=0`, authentication must belong to the newly initiated attempt, but +its age need not remain zero while the user reviews the draft. Enforce your application's +allowed freshness window at execution time. + +## Headless agents {#headless-agents} + +The [device flow](/world-id-auth/connect-your-backend#sign-in-a-headless-agent-device-flow) +already requires fresh authentication and explicit authentication approval; it does not +implement `max_age` or `prompt`. The user approves authentication for the requesting +client, and that approval does not cover a business action such as publishing a +draft. Collect separate action approval in your application only if your policy requires it. + +For an agent calling your API or MCP server, your backend issues and validates +its own access credentials. Where supported, use the +[authentication challenge and retry flow](/world-id-auth/step-up#challenge-an-agent-or-api-client-rfc-9470) +to request a fresh checkpoint. Keep the operation blocked until your backend +validates authentication, checks permissions, and confirms any action approval +required by your application. diff --git a/world-id-auth/id-tokens-and-identity.mdx b/world-id-auth/id-tokens-and-identity.mdx new file mode 100644 index 0000000..fcb0722 --- /dev/null +++ b/world-id-auth/id-tokens-and-identity.mdx @@ -0,0 +1,137 @@ +--- +title: "ID tokens and identity" +sidebarTitle: "ID tokens and identity" +description: "Validate token claims, read UserInfo, and recognize accounts using the issuer and pairwise subject." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/id-token.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +This guide describes the OIDC token response, the ID token's claims, and how to +store and compare the subject (`sub`). + +## Token response {#token-response} + +Browser and device flows both return `id_token`, `access_token`, `token_type`, and +`expires_in`: + +- **`id_token`:** the signed authentication result described below. + [Validate it](/world-id-auth/connect-your-backend#validate-the-id-token) before using + any claim. +- **`access_token`:** an opaque credential for the [UserInfo endpoint](#userinfo). + It does not authorize calls to the World ID MCP server or your application's APIs. +- **`token_type`:** `Bearer`. +- **`expires_in`:** the access token's lifetime in seconds. Read the returned + value. The ID token has its own expiration, given by `exp`. + +World ID authentication with `openid` does not issue refresh tokens. The +[XAA profile](/world-id-auth/xaa) returns a rotating refresh token for +`openid offline_access`. + +After validation, your backend creates its own application sessions and API or +MCP credentials. Keep the ID token and subject in your backend, outside model +messages and tool results. + +## Decoded ID token {#decoded-id-token} + +The `id_token` is a signed JWT with three parts: `header.payload.signature`. Its +header identifies the RS256 signing algorithm and the signing key by its `kid`. +Use an OIDC library to verify the signature with the keys advertised by the +[discovery document](https://auth.world.org/.well-known/openid-configuration). Decoding the +payload alone does not validate it. + +This example shows a decoded payload for a browser authorization request that +included a nonce. Identifiers and timestamps are illustrative; validate the +returned `exp` rather than assuming a fixed lifetime. + +```json +{ + "iss": "https://auth.world.org", + "sub": "", + "aud": "", + "exp": 1790000420, + "iat": 1790000120, + "jti": "", + "auth_time": 1790000000, + "acr": "https://world.org/oidc/acr/orb", + "amr": ["pop"], + "nonce": "" +} +``` + +## Claim reference {#claim-reference} + +| Claim | Meaning and use | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `iss` | Issuer. Require an exact match to your configured World ID issuer and store it with `sub`. | +| `sub` | The World ID account's stable identifier within your registered sector. Use it for account recognition after token validation. | +| `aud` | Intended client. Require your registered client ID as the audience. Sharing a sector does not make another client's token valid for yours. | +| `exp` | Token expiration, as Unix time in seconds. Reject expired tokens. | +| `iat` | Token issuance time, as Unix time in seconds. A new token can reuse an older authentication. | +| `jti` | Token identifier. Use `sub` to recognize the account; `jti` does not identify the user or approve a business action. | +| `auth_time` | Authentication time, as Unix time in seconds. Use this to enforce your freshness requirement. | +| `acr` | Achieved authentication class: `https://world.org/oidc/acr/orb`, `https://world.org/oidc/acr/orb-face`, or, only when you request it, `https://world.org/oidc/acr/selfie`. Check it against your requirements. | +| `amr` | `["pop"]` for a World ID proof, `["pop", "face"]` for Face Auth or Selfie Check, or `["pop", "user", "mfa"]` for a user-verified passkey. Only the passkey value asserts MFA. | +| `nonce` | Matches the nonce your backend sent in the browser authorization request. Present only when supplied; omitted in device grants. | + +For example, if a user authenticates at 10:00 and their existing session produces +a token at 11:00, `auth_time` still represents 10:00 while `iat` represents 11:00. +Use `auth_time` for authentication freshness, as described in +[Step-up authentication](/world-id-auth/step-up). + +For accounts with an enabled inbox, newly issued ID tokens and ID-JAGs also +include `email` and `email_verified: true`. Clients may request the `email` scope; +enabled aliases are included whether or not it is requested. The immutable address is +`@`, using the subject for the receiving application's +sector. Clients sharing a sector share an address; different sectors receive +different addresses. An ID-JAG uses the target app's subject and address. The +email claim may support partner account provisioning, but does not authorize +reading the inbox or connecting an app. +Deleting an account permanently retires its aliases. If an account's subject +matches a retired alias, authentication still works but its email claim is omitted. +Keep `(iss, sub)` as the account identity and require explicit account linking. + +Name, raw World identifiers, and World proofs are not included. See the +[OIDC claim definitions](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) +and [identity stability rules](https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability). + +## UserInfo {#userinfo} + +Send `GET` or `POST` to the discovery document's `userinfo_endpoint` with +`Authorization: Bearer `. UserInfo returns `sub` and, when the +account's inbox is enabled, `email` and `email_verified`. Require its `sub` to +match the validated ID token before using the claims. Cookies, ID tokens, and +query-string credentials do not authenticate this endpoint. + +Access tokens expire within five minutes. UserInfo reads from a database follower, +so revocation and account, client, consent, or XAA grant changes take effect after +replication. A stale follower can accept a revoked token until replication catches +up or the token expires. Reauthorizing a revoked connection does not revive its +old tokens after replication. The authenticated issuing client can revoke a token +at the discovery document's `revocation_endpoint`. +Revoking an XAA access token revokes its delegation family. + +## Store and match sub {#store-and-match-sub} + +After validating the ID token, store **`(iss, sub)`** as the external identity +linked to your application's account. Link World ID through an explicit action while +the user is signed in to that account. For later step-up, require the returned +issuer and subject to match the stored pair before updating the session's assurance. + +Treat `sub` as an opaque, case-sensitive string. Compare the complete value; +do not decode it or derive your own replacement. A subject supplied by a +browser or agent is not proof of authentication. + +For the same issuer and registered sector, the same World ID account receives +the same `sub` across browser sign-ins, devices, and agent authentication flows. + +A **sector** is the registered scope in which applications share subject identifiers. +The subject is **pairwise**: different sectors receive different values for the +same account. Clients deliberately registered in the same sector receive the +same value. Rotating a client secret or using a new client ID within that sector +does not change the subject. Choose your sector before integrating; see +[Manage credentials and callbacks](/world-id-auth/app-registration#manage-credentials-and-callbacks). + +A new World identity can produce a new subject. Do not automatically merge +accounts with different subjects or infer a mapping between sectors. diff --git a/world-id-auth/quickstart.mdx b/world-id-auth/quickstart.mdx new file mode 100644 index 0000000..ad0c812 --- /dev/null +++ b/world-id-auth/quickstart.mdx @@ -0,0 +1,108 @@ +--- +title: "Quickstart" +sidebarTitle: "Quickstart" +description: "Register an application, connect to the World ID issuer, and add step-up authentication." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/getting-started.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +import AuthEnvironment from "/snippets/world-id-auth-environment.mdx"; + + + +A person uses the World ID app to authenticate with their World ID. The World ID +issuer verifies that authentication and issues a signed ID token to your backend. +Your application uses the token to recognize the same World ID and determine when the person last +authenticated. Your application keeps its own accounts, sessions, permissions, +and actions. + +## How it works {#how-it-works} + +Your backend connects to the World ID issuer with OpenID Connect (OIDC). After +the person authenticates in the World ID app, your backend validates the returned +ID token and reads two claims: + +- **`sub`:** a stable identifier for the person's World ID account, private to your + application's sector. Store it with the issuer as `(iss, sub)` to recognize the account. +- **`auth_time`:** when the person authenticated. Use it to require recent + authentication before sensitive actions. + +See [ID token and sub](/world-id-auth/id-tokens-and-identity) for the full claim reference. + +## Integrate in four steps {#integrate-in-four-steps} + +The recommended integration keeps your existing sign-in and adds a World ID check +before sensitive actions: + +1. **[Register your application](/world-id-auth/app-registration)** and save its + credentials in your backend. +2. **Connect to the World ID issuer** through [your backend](/world-id-auth/connect-your-backend) or + [your auth server](/world-id-auth/connect-your-auth-server). +3. **Add [step-up authentication](/world-id-auth/step-up)** to require a + recent World ID check before an action. +4. **For agent workflows, add a [human checkpoint](/world-id-auth/human-in-the-loop).** + Pause the workflow and validate fresh World ID authentication before resuming. + +Step-up checks authentication freshness. A human checkpoint uses that check to +pause and resume an agent workflow. Both work with your existing sign-in or, +optionally, with [World ID as your primary sign-in](/world-id-auth/connect-your-backend#primary-sign-in). + +**Device authentication with `openid` always requires fresh authentication.** The person +uses an enrolled passkey or approves a proof request in the World ID app, according +to the requested authentication class. Device grants with `openid` never reuse a browser session. +[XAA device consent](/world-id-auth/xaa#connect-the-agent) can use an +account session authenticated after the device request began. See +[freshness and assurance](/world-id-auth/step-up#assurance-values) for what +the returned authentication claims mean and how to validate them. + +Before calling the integration complete, [test it end to end](/world-id-auth/testing). +To test without visiting an Orb, use the +[sandbox setup](/world-id-auth/testing#test-in-the-sandbox). + +## Environment values {#environment-values} + +| Setting | Value | +| ---------------- | ------------------------------------------------------------------------------------------- | +| Issuer | `https://auth.world.org` | +| Discovery | `https://auth.world.org/.well-known/openid-configuration` | +| Developer portal | `https://auth.world.org/portal` | +| Scope | `openid`; `openid offline_access` for XAA; optional `email` | +| Grants | Authorization code with S256 PKCE; device authorization; refresh and token exchange for XAA | +| ID token signing | RS256 | + +## Other World ID integrations {#other-world-id-integrations} + +These guides cover the World ID issuer's OIDC integration. IDKit proof applications use +different registrations, credentials, and identifiers; do not mix +them with this issuer's discovery-based flow. + +- **IDKit uniqueness:** use a nullifier scoped to the application and action for one-use eligibility, + not an OIDC `sub`. Verify the proof on your backend and atomically reject a + previously consumed nullifier. A stored uniqueness result does not establish + fresh authentication. Follow the + [IDKit setup and verification guide](https://docs.world.org/world-id/idkit/integrate). +- **IDKit sessions:** use repeated proofs of the saved `session_id` for account + continuity. Each check still requires user interaction and a fresh proof. + `session_nullifier` identifies a proof for replay checks; it is not an account + ID. A proof session is not an application login cookie or an issuer browser session. Follow + the [session-proof guide](https://docs.world.org/world-id/idkit/session-proofs) + for registration, signed requests, verification, and replay checks. + +**Legacy Sign In with World ID is deprecated.** Use the World ID issuer's OIDC flow for +new sign-in integrations; legacy credentials and identities are not interchangeable +with the World ID issuer's. + +Use the selected product's credential, environment, request-expiry, and replay +rules; its name alone does not establish assurance or business permission. +OIDC authentication remains repeatable. For one-use benefits, enforce your own rule with +atomic consumption in the +[appropriate identity scope](/world-id-auth/id-tokens-and-identity#store-and-match-sub). + +## Get help from a coding agent {#get-help-from-a-coding-agent} + +Give your coding agent the public guides to help implement your integration. +No plugin or MCP connection is required to read them. For optional app registration +and management through tools, connect the developer MCP server. See +[Set up your coding agent](/world-id-auth/build-with-a-coding-agent). diff --git a/world-id-auth/step-up.mdx b/world-id-auth/step-up.mdx new file mode 100644 index 0000000..50acf0f --- /dev/null +++ b/world-id-auth/step-up.mdx @@ -0,0 +1,276 @@ +--- +title: "Step-up authentication" +sidebarTitle: "Step-up authentication" +description: "Require recent World ID authentication before sensitive actions." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/step-up.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +Require recent World ID authentication before a sensitive action in your +application. +Your backend validates recent authentication, links or matches the returned +World ID to the application account, then checks permissions before allowing the +action. + +Before starting, [connect your backend](/world-id-auth/connect-your-backend) or +[configure your auth server](/world-id-auth/connect-your-auth-server). +Link World ID before step-up, or explicitly authorize the flow below to enroll +the first validated identity for the signed-in account. +This works with your existing sign-in or World ID as your primary sign-in. + +## Decide who may authorize the operation {#decide-who-may-authorize-the-operation} + +Bind each attempt to its initiating session, target account, and exact operation +or resource before authentication. If the account already has a linked World ID, +require the validated `(iss, sub)` to match. If the account is unlinked, enroll +the validated identity only when the saved attempt explicitly permits enrollment. + +Some operations may accept any qualifying human or a user with a specific role. +Define the required assurance and eligibility, and whether a different identity +is allowed. Test both acceptance and rejection; a valid identity alone is not +authorization. + +## Implement step-up {#implement-step-up} + +1. **Save the pending operation.** Record the current application account, + its linked + World ID `(iss, sub)` if present, whether this attempt may enroll an identity, + the required authentication age and class, and the pending operation in + server-side state. Bind a fresh OAuth `state`, `nonce`, and PKCE verifier to + this attempt. Do not execute the operation yet. +2. **Request fresh authentication.** Redirect to the authorization endpoint with the + usual [browser authentication](/world-id-auth/connect-your-backend#sign-in-through-the-browser) + parameters plus `max_age=0` for fresh authentication, or a positive `max_age` for a + maximum authentication age. For example, these additional decoded query + fields request a supported class and a new proof: + + ```text + max_age=0 + acr_values=https://world.org/oidc/acr/orb + ``` + +3. **Validate the result.** Validate the callback `state`, redeem the code once, + and [validate the ID token](/world-id-auth/connect-your-backend#validate-the-id-token), + including its nonce. Reconfirm that the initiating session and target account + match the saved attempt. For a linked account, require the returned + `(iss, sub)` to match. For an unlinked account, continue only when the attempt + explicitly permits enrollment and the identity is not linked elsewhere. Defer + storing the pair until the remaining checks pass. +4. **Check freshness and assurance.** Validate `acr`, `amr`, and `auth_time` + against the saved policy. For `max_age=N`, check the authentication age when the + operation runs, allowing only a small, explicit clock tolerance and rejecting + implausibly future timestamps. For `max_age=0`, check that authentication + belongs to the newly initiated attempt (using its start time, nonce, and bounded + lifetime); do not require its age to remain literally zero during the browser + round trip. +5. **Allow the operation.** After all checks pass, atomically save the validated + `(iss, sub)` for enrollment if needed, but only while the target account is + still unlinked and the identity is still unassigned. Then update your trusted + session or grant's assurance, recheck permissions and any transaction-specific + approval, and allow the + intended operation. Cancellation, expiry, changed binding, identity mismatch, + unsupported assurance, and dependency failure leave the operation + unauthorized. + +Authentication alone does not authorize a payment, redemption, or account change. +For an agent workflow that also requires approval of the action, follow the +[Human in the loop guide](/world-id-auth/human-in-the-loop). + +## Request parameters {#request-parameters} + +Step-up uses the same registered client, `scope=openid`, state, and S256 PKCE +requirements as [browser authentication](/world-id-auth/connect-your-backend#sign-in-through-the-browser), +at the authorization endpoint from the +[OIDC discovery document](https://auth.world.org/.well-known/openid-configuration). The +protocol permits omitting nonce; the step-up procedure above uses it to bind the +result to the attempt. + +| Parameter | Implemented behavior | +| -------------- | ------------------------------------------------------------------------------------------------------------------ | +| `max_age=N` | Accept authentication no older than N seconds, subject to session/consent eligibility. N is a nonnegative integer. | +| `max_age=0` | Require fresh authentication for this transaction, even with an existing browser session. | +| `prompt=login` | Require fresh authentication; overrides a larger `max_age`. | +| `prompt=none` | Forbid interaction; return `login_required` or `interaction_required` if silent authorization cannot complete. | +| `acr_values` | Voluntary preferences. Unsupported values do not force an error; validate the achieved `acr` yourself. | +| `claims` | Request an essential ID-token `acr` to require an acceptable authentication class. | + +Omit `prompt` for normal interactive fallback. The supported values are `login`, +`none`, and `consent`, supplied individually. `consent` forces the interactive +authorization page. `select_account` and prompt combinations are rejected. +Do not combine `prompt=none` with a requirement for fresh authentication. + +## Require Face Auth {#require-face-auth} + +To require a selfie check in the World ID app, send this JSON as the URL-encoded +`claims` parameter: + +```json +{ "id_token": { "acr": { "essential": true, "values": ["https://world.org/oidc/acr/orb-face"] } } } +``` + +Add `max_age=0` for a check during this browser authentication attempt. Without a +freshness requirement, a qualifying Face Auth session can be reused. + +`acr_values=https://world.org/oidc/acr/orb-face` requests a selfie whenever a +new proof is created. Unlike an essential `claims` request, it permits reuse of +a baseline browser session and acceptance of a baseline result. When listing +multiple `acr_values`, put your preferred class first; the first supported class +determines whether a new proof requires Face Auth. + +An explicit `claims.id_token.acr` request takes precedence over `acr_values`. +Multiple acceptable values are alternatives; only one needs to match. If none +of the requested essential classes is supported, the request fails with +`unmet_authentication_requirements`. The RP must still validate the returned `acr`. + +When passkeys are enabled, requests offer authentication with an enrolled passkey +first unless their first supported class is `https://world.org/oidc/acr/orb-face`. +A passkey login reports the account's class, `https://world.org/oidc/acr/orb` or, for a +Selfie Check account, `https://world.org/oidc/acr/selfie`, but never Face Auth. Check +`amr` to see whether a World ID proof or a passkey authenticated the person. Add +`max_age=0` when authentication must be fresh for this browser attempt. + +The device authorization endpoint accepts the same `acr_values` and `claims` +parameters as extensions to its RFC 8628 profile. Device authentication always +requires fresh authentication; `prompt` and `max_age` do not control this grant. If the +mobile response cannot satisfy an essential requirement, token polling returns +`access_denied`, never a downgraded token. + +A requested `claims.id_token.sub` value must match the authenticating account. +A mismatch ends the request with `access_denied`, including device token polling. + +Reference: [OIDC ACR requests](https://openid.net/specs/openid-connect-core-1_0.html#acrSemantics). + +## Default verification preference {#default-verification-preference} + +A request with neither `acr_values` nor an `acr` claim request uses the +client's registered default classes as its `acr_values`. This is useful when +your OIDC library or identity broker cannot add `acr_values`. + +In the [developer portal](https://auth.world.org/portal), open your application and go to +**Settings → Authenticate → Default verification preference**. Choose: + +- **World ID default (currently Orb):** clear the client override and use the World ID default. +- **Orb:** prefer Orb authentication. +- **Prefer Orb with Face Auth:** prefer a selfie check when creating a new World ID proof. + +These defaults are voluntary preferences. An eligible Orb browser session can +still be reused. To require Face Auth, send an +[essential `acr` claim](/world-id-auth/step-up#require-face-auth). + +## Assurance values {#assurance-values} + +- **`acr`:** `https://world.org/oidc/acr/orb` means the account holds a verified + Orb credential, and the person authenticated with a proof of that credential or + an enrolled passkey. `https://world.org/oidc/acr/orb-face` additionally requires + the World ID app to report a completed Face Auth check for the proof request. +- **`amr`:** `["pop"]` for a World ID proof; `["pop", "face"]` for Face Auth. + `["pop", "user", "mfa"]` identifies a user-verified passkey, which does not assert + a fresh World proof. Only the passkey value asserts MFA. +- **`auth_time`:** server verification time for the credential proof or passkey assertion, not + the phone's exact proof-generation or biometric-check time. Browser-session reuse + preserves it and the original `acr` and `amr`; a new token's `iat` does not imply + a fresh proof. + +Face Auth assurance trusts the official app's completion signal together with +the verified proof. The server does not independently verify the selfie check. +A missing or false completion signal cannot satisfy the Face Auth class, +including when the signal is missing from an older app. + +## Authentication and authorization {#authentication-and-authorization} + +World ID proof authentication asks the person to approve a request in the World ID app +on their phone. The World ID issuer verifies the credential proof for that transaction +before it can issue a browser code or approve a device grant. Possession of the +verification link alone cannot complete authentication. + +After proof verification, `/approve` finalizes authentication. The initiating +browser and the verification page call it automatically; in the device flow, +your application can also submit it. The phone proof remains required. Only the +initiating browser can claim a browser transaction's session and callback URL. +Device grants create no browser session; token redemption requires the private +device code and the registered client's authentication. + +An eligible request can instead authenticate with an enrolled passkey. The WebAuthn +assertion endpoint verifies user presence and user verification and issues the +browser code or approves the device grant. This does not require a phone proof +or a separate `/approve` call. + +The ID token +contains authentication context, not terms for a separate business operation or +a signed action digest. If your application needs approval for a payment, +redemption, or account change, enforce and record that approval separately. + +## Prevent stale or duplicate operations {#prevent-stale-or-duplicate-operations} + +- **Save what the user is approving.** When authentication starts, save the + operation, target account, authentication requirements, and expiry. Do not + change them. If the operation or target changes, start again. Checking status + or retrying must not extend the expiry. +- **Check again before acting.** Verify that the session, account, World ID, and + permissions still match the saved attempt. Reject an attempt that is outdated, + cancelled, or expired. +- **Use an approval once.** Mark the approval as used in the same database + transaction that authorizes or reserves the operation. A cancellation blocks + the operation only if it is saved first; it cannot undo an operation that + already happened. +- **Revoke access in your application.** Check expiry and revocation before each + use. Ending the World ID issuer's browser session, deleting the OIDC client, rotating credentials, + revoking an MCP grant, or closing a browser tab does not revoke access stored + by your application. +- **Do not blindly retry external requests.** Give each external operation a + stable ID. After a timeout or lost response, ask the external service whether + it completed the operation. Retry with the same ID only if the service + guarantees that duplicates cannot run twice. If you cannot determine the + result, stop and investigate; do not authenticate again to repeat it. + +## Through your auth server {#through-your-auth-server} + +The chain is: application or agent → your auth server → World ID issuer → your +auth server → your protected operation. Each layer must preserve the requirement. + +- **Start a new upstream request.** Your auth server must start a new OIDC + authorization request with the World ID issuer for each attempt. Its existing + session must not bypass the upstream freshness check. +- **Forward the parameters.** Pass `max_age` and any requested `prompt=login`, + `acr_values`, and `claims` parameters to `https://auth.world.org/api/v1/authorize`. A setting + on only the outer login request is insufficient if the connector drops it. +- **Keep the issuer's result.** Your auth server must validate and retain the + issuer's `auth_time`, `acr`, `amr`, and account binding. Convey the result to the + resource through trusted server state or your own signed token claims. A login, + password or MFA event at your auth server, MCP consent, OAuth refresh, or new + downstream token does not prove a fresh World ID authentication. +- **Enforce at the resource.** If your auth server cannot propagate or validate + the requirement, this integration cannot provide World ID step-up. + +See [Connect the World ID issuer to your auth server](/world-id-auth/connect-your-auth-server) +for configuration. A useful acceptance test starts with an old issuer browser +session and a live session on your auth server: the sensitive operation must cause a +fresh upstream proof, produce a newer `auth_time`, and remain blocked when the +human cancels or uses the wrong account. + +## Challenge an agent or API client (RFC 9470) {#challenge-an-agent-or-api-client-rfc-9470} + +Your resource server can reject insufficient authentication with a header like: + +```http +HTTP/1.1 401 Unauthorized +WWW-Authenticate: Bearer error="insufficient_user_authentication", max_age="300", acr_values="https://world.org/oidc/acr/orb" +``` + +This is a response your downstream resource implements, not an endpoint offered +by the World ID issuer. The client asks your auth server for sufficient +authentication; that server carries the requirement upstream as above. After +validation, your server issues a new resource-bound credential and the client +retries the operation. The resource checks the new credential's permissions and +authentication context, including freshness at execution time. Do not send the +ID token from the World ID issuer directly to a downstream MCP server. + +Both the client and your auth server must support this challenge and retry +flow; basic MCP OAuth support does not guarantee RFC 9470 support. Where a client +cannot handle it, provide an explicit interactive reauthentication path and +retry only after the backend confirms completion. Bound retries, and ensure a +mutating operation cannot execute twice after an uncertain response. + +Reference: [RFC 9470](https://www.rfc-editor.org/info/rfc9470/). diff --git a/world-id-auth/testing.mdx b/world-id-auth/testing.mdx new file mode 100644 index 0000000..7a68a47 --- /dev/null +++ b/world-id-auth/testing.mdx @@ -0,0 +1,107 @@ +--- +title: "Test your integration" +sidebarTitle: "Test your integration" +description: "Test World ID integration in the sandbox, complete real flows, and check failure paths." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/testing.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +Use the checks that apply to your integration and authorization policy. + +## Test in the sandbox {#test-in-the-sandbox} + +The sandbox lets you test the OIDC flow with a sandbox World ID that has an +Orb-level test credential, without visiting an Orb. Sandbox authentication does +not establish that a real person is Orb-verified. + +To try World ID flows before you register an application, use the +[OAuth Playground](https://oauth-playground.toolsforhumanity.dev/) with a +[sandbox World ID](/world-id-auth/testing#create-a-sandbox-world-id). +The playground runs authentication, step-up, device, XAA, and MCP flows against +the sandbox issuer and shows what a relying party should validate. + +### Set up your application {#set-up-your-application} + +Use the sandbox environment for registration and authentication: + +| Setting | Sandbox value | +| ---------------- | ----------------------------------------------------------------- | +| Issuer | `https://sandbox.auth.world.org` | +| Discovery | `https://sandbox.auth.world.org/.well-known/openid-configuration` | +| Developer portal | [Sandbox portal](https://sandbox.auth.world.org/portal) | + +Sign in to the sandbox portal with Google and +[register your application](/world-id-auth/app-registration). +Configure your application with the registration's credentials and the sandbox +discovery document. Production registrations, credentials, and World IDs are +separate. Keep sandbox account mappings separate from production, and store +validated +identities as `(iss, sub)`. + +### Create a sandbox World ID {#create-a-sandbox-world-id} + +1. **Get access to the sandbox World ID app.** Ask your World ID integration + contact for access. On iOS, install the sandbox app from the TestFlight + invitation. Confirm that it is the sandbox build. +2. **Create the test identity.** In the sandbox app, open the middle World ID + tab, choose **Add proof of human**, then **Manual verify**. Take a selfie or + upload one from your camera roll and complete the prompted flow. The exact + labels may vary by app version. This creates a sandbox World ID; you do not + need an Orb appointment. +3. **Allow about one hour for issuance.** The **Human** card can appear before + the identity is ready. Authentication can fail during this wait, even though + setup looks complete in the app. +4. **Authenticate through your application.** Start a new authentication attempt + using the sandbox issuer and approve it with the sandbox app. Use the + end-to-end checks below to validate the result in your backend. + +If scanning a QR code or opening an authentication link fails, check that both +your application and the World ID app use the sandbox environment, and that +about an hour has passed since identity setup. If it still fails, share the +error and the time you completed setup with your integration contact. + +Before launch, also complete the production flow with the production World ID +app and a real World ID. Sandbox results do not demonstrate production identity +verification. + +## Complete a real flow {#complete-a-real-flow} + +1. **Use your registered application.** Use the issuer, discovery, and credentials from + the same environment. Check your [registration](/world-id-auth/app-registration) + before starting. +2. **Authenticate and validate.** Connect your World ID through your + browser or device flow. Confirm that your backend or auth server + [validates the ID token](/world-id-auth/connect-your-backend#validate-the-id-token). +3. **Use the result in your application.** For primary sign-in, create the application session and + make an authenticated request as the expected account. For step-up or a human + checkpoint, confirm that the protected operation stays blocked until your + backend accepts the authentication and any permissions or action approval + your policy requires. Then perform the operation. +4. **Check the final outcome.** If the operation calls another service, confirm + its result there. A queued request or an application receipt alone does not establish + that a payment settled or another external action completed. + +Exercise each browser, device, or auth-server path you intend to support. For step-up through your auth server, include the [existing-session acceptance test](/world-id-auth/step-up#through-your-auth-server). +For separate action approval, test the [application-owned approval flow](/world-id-auth/human-in-the-loop#optional-collect-action-approval-in-your-application). + +## Check failure paths {#check-failure-paths} + +Check both the response shown to the user and your backend's session, permission, +or operation state. Use the existing [OIDC validation rules](/world-id-auth/connect-your-backend#validate-the-id-token) +and [step-up procedure](/world-id-auth/step-up#implement-step-up) as the +source of expected behavior. + +| Check | Expected outcome | +| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Cancel or expire a pending attempt before it authorizes access. | The attempt grants no new access and does not execute the protected operation. | +| Present an invalid token or a result bound to another attempt. | Validation fails; no new session or permission is created from that result. | +| Authenticate with a different account. | Existing-owner step-up does not satisfy the original account's requirement. For other policies, test acceptance and rejection according to the account or role your application requires. | +| Retry a consumed code or repeat a one-use operation. | The code cannot be redeemed again. Where your application promises one-use execution, retries and concurrent requests do not execute it twice. Repeated World ID authentication remains allowed. | +| Interrupt a dependency call or lose its response. | The application does not treat an error or unknown outcome as success. An uncertain external action is reconciled before a retry could duplicate it. | + +Use local signed fixtures to exercise validation failures and application policy without +requiring a person for every test. Keep them isolated from live credentials and +execution. Fixture tests do not demonstrate a real World ID proof or a complete +issuer-backed flow. diff --git a/world-id-auth/use-world-id-with-your-agent.mdx b/world-id-auth/use-world-id-with-your-agent.mdx new file mode 100644 index 0000000..e415be8 --- /dev/null +++ b/world-id-auth/use-world-id-with-your-agent.mdx @@ -0,0 +1,88 @@ +--- +title: "Use World ID with your agent" +sidebarTitle: "Use World ID with your agent" +description: "Understand World ID agent permissions, discover benefits, and manage your connections." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/world-id-users.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +When you authenticate with World ID and approve an agent's OAuth access, the agent +can read the authenticated version of this guide through the user MCP server. +This public guide contains the same general guidance, not private account data. + +## What you authorized {#what-you-authorized} + +With the `world-id:read` scope on `https://auth.world.org/mcp`, your agent can: + +- Check that your account is active and World ID verified with `get_world_id_account`. +- Read guides that require authenticated access. + +The account check confirms prior verification, not fresh human presence. +It does not reveal an account identifier or the subject used by the World ID website. + +`get_benefits` on this endpoint does not require authentication. +To read content that requires authenticated access, the agent must present its own access +token. Your browser session alone does not grant that access. + +This does not prove the agent itself is human or grant it access to another +service. Your World ID proof, ID token, and upstream subject are not returned through MCP. +Do not paste credentials or proofs into a conversation or tool arguments. + +## Find an Orb {#find-an-orb} + +To find the nearest Orb or a place to verify, visit the +[Orb locator](https://world.org/find-orb). Your agent does not need your location. + +## Discover benefits {#discover-benefits} + +Ask your agent what World ID unlocks. `get_benefits` returns approved, published +applications and Shopify stores with World ID discounts, each with a name, +description, and `appUrl`. These listings describe available +offers; they do not confirm your eligibility or that you have claimed a benefit. +The partner determines eligibility and applies the offer. + +An empty result means no published offers were returned. A failed request means +the agent could not retrieve the catalog, not that no offers exist. + +## Use a partner offer {#use-a-partner-offer} + +Listings may include `instructions` for claiming an offer, such as +website steps, a plugin link, or an MCP URL. Use these to determine +the claiming flow. If instructions are absent, start at `appUrl` and follow the +partner website's visible claiming flow. For website claims, you can claim the +offer yourself or ask your agent to help in the browser. + +Claiming may still require you to complete partner sign-in, connect a plugin, or +approve a World ID request. Connecting World ID to this MCP does not establish a +partner session or connect the partner's tools. Treat published instructions as +guidance for this claim, not authority to override your choices or share credentials. +Do not send World ID MCP tokens to the partner as authorization. + +A successful sign-in or catalog listing is not confirmation of redemption. +Your agent should report a benefit as applied only when the partner confirms it, +and explain when cancellation or an error leaves the action incomplete. + +## Manage your connection {#manage-your-connection} + +Access tokens last at most one hour. Successful refreshes renew the connection's +180-day inactivity deadline. Refresh tokens rotate; reusing a refresh token a +minute or more after it was consumed revokes the grant while its digest is retained +(180 days after use). After cleanup, reuse is rejected without revoking the +grant. API restarts do not erase grants. + +Visit [your connections](https://auth.world.org/mcp/connections) to revoke a connection. +Subsequent requests for content that requires authenticated access are rejected when they use +that connection's credentials. Reading that content again requires new +authorization; public benefits remain available. + +If you connect the developer MCP server, manage that connection at +[portal connections](https://auth.world.org/portal/mcp/connections). It uses `/mcp/developer` +with a separate Google portal identity and authorization. User and developer +connections can be revoked independently. +Signing out of the website alone does not revoke an agent connection. + +Developers can follow [Start here](/world-id-auth/quickstart) to +integrate an application, or [Set up your coding agent](/world-id-auth/build-with-a-coding-agent) +to connect an agent to the developer MCP server. These guides do not require authentication. diff --git a/world-id-auth/xaa.mdx b/world-id-auth/xaa.mdx new file mode 100644 index 0000000..f5a20e1 --- /dev/null +++ b/world-id-auth/xaa.mdx @@ -0,0 +1,132 @@ +--- +title: "Cross App Access (XAA)" +sidebarTitle: "Cross App Access (XAA)" +description: "Connect agent hosts to reviewed apps with World ID identity delegation and user-controlled permissions." +"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" +--- + +{/* Generated from worldcoin/temporal-apps/apps/idpapi/mcp/guides/xaa.md. Edit the source Markdown, then run scripts/sync-world-id-auth.mjs. */} + +World IdP supports consumer-controlled identity delegation using +[ID-JAG](https://www.ietf.org/archive/id/draft-ietf-oauth-identity-assertion-authz-grant-04.html). +A user connects an agent host and chooses the apps it may access. The host can +then obtain credentials for those apps without separate World ID authentication +for each app. Each app's authorization server issues its own access tokens and +controls API permissions. The app does not need OIDC sign-in federation. + +This guide covers app integration and user permissions. The host's MCP +authentication layer handles World sign-in, ID-JAG exchange, token renewal, +and credential storage. The model calls MCP tools and can help developers +implement the integration. Credentials stay outside model context, tool results, +and conversations. + +## Requirements {#requirements} + +The agent host must support XAA and allow World to be configured as its identity +provider. A plugin cannot supply missing host authentication support. + +Check the issuer's [discovery document](https://auth.world.org/.well-known/openid-configuration) +for XAA availability. It advertises `offline_access`, refresh, and ID-JAG exchange +only while XAA is enabled. When disabled, requests for `offline_access` receive +`invalid_scope`; refresh and ID-JAG exchange return HTTP 400 +`unsupported_grant_type`. These errors do not include `Retry-After`. Saved +permissions remain intact, and users can still remove access or disconnect. + +Your app's authorization server must accept World-issued ID-JAGs and issue access +tokens for your MCP server. A remote MCP server is sufficient; a local MCP server +or a World proxy is not required. + +World's own MCP tools use separate connections. Connecting to those tools does +not authorize the host to access other apps through XAA. + +## Configure your authorization server {#configure-your-authorization-server} + +Configure your authorization server to trust the World issuer, `https://auth.world.org`. +Use its [discovery document](https://auth.world.org/.well-known/openid-configuration) to find +the signing keys through `jwks_uri`. + +Accept the ID-JAG JWT bearer grant. Validate issuer, audience, signature, expiry, +replay, and resource restrictions, and bind its `client_id` to the authenticated +host. Hosts and authorization servers must agree on the client identifier; +for a host using a Client ID Metadata Document (CIMD), this is the exact HTTPS +metadata URL. Client credentials used by the host at your authorization server +must be separate from its World credentials. + +Keep the metadata URL stable. New CIMD URLs receive separate host identities, +even on the same hostname; existing OIDC clients retain their stored identities. + +The ID-JAG carries the app's existing World pairwise `sub`. Its authentication +time and assurance describe the original World authentication. Match accounts +using the [World issuer and subject](/world-id-auth/id-tokens-and-identity#store-and-match-sub). +Your authorization server applies its own scope policy and issues the access +token used to call your MCP server. + +Publish standard RFC 8414 metadata at your authorization server's well-known +location, advertising ID-JAG support: + +```json +{ + "issuer": "https://app.example", + "grant_types_supported": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], + "authorization_grant_profiles_supported": ["urn:ietf:params:oauth:grant-profile:id-jag"] +} +``` + +## Enroll your app {#enroll-your-app} + +Register your app in the [developer portal](https://auth.world.org/portal). As its owner, use +the active app's client settings to submit your authorization-server issuer and +the resource URLs of your MCP servers. The issuer identifies the server that +issues access tokens; it is distinct from the app's World identity sector. + +World reviews your app's authority over its identity sector, issuer, and resources +before users can select it. Enrollment uses the standard metadata above; no +additional verification document or custom metadata field is required. XAA review +is independent of catalog publication, and the app picker uses the name recorded +at approval. + +Several apps can share an authorization server, with separate resources and +permissions. A resource URL may be approved for only one app; pending or rejected +submissions do not reserve it. World IdP's own authorization server cannot enroll +as an app's target authorization server. + +Changing the issuer or resources requires a new review. Resource changes preserve +saved app permissions; changing the issuer requires renewed user permission. +Disabling the app or withdrawing approval stops new ID-JAGs immediately. +Rejected enrollments can request review again without changing the binding or +requiring users to repeat app selection. + +## Connect the agent {#connect-the-agent} + +The host opens World sign-in and consent. The user reviews the agent and selects +which apps it may access. On the first connection, no apps are selected. New apps +require explicit selection. Users can connect without selecting apps and choose them +later at [Cross-app connections](https://auth.world.org/xaa/connections). + +The connection approval link can be opened on the user's own device. Only the +browser that started the connection can claim its callback. Account sign-in must +complete in the browser that started that sign-in; use the host's connection +approval link for a handoff to another device. + +Authorization expires after **180 days without a successful token refresh or +ID-JAG exchange**. +The host reconnects interactively after expiry or revocation. Device consent +requires an account session authenticated at or after the device request began. + +Permissions belong to the exact host client ID. Sessions using that client ID share +app permissions, and reconnecting preserves saved choices. Temporarily unavailable +apps retain those choices while their authorization-server issuer stays unchanged. + +## Manage access {#manage-access} + +Users can remove individual app permissions or disconnect the host at +[Agents](https://auth.world.org/xaa/connections). Removing app access ends all of +that user's World grants for the host and removes its app approval and related +agent permissions. Users can save new app choices before reconnecting. +Browser logout does not disconnect the host. + +Removing an app permission immediately stops new ID-JAGs for that app. +Disconnecting stops them for every app. Already-issued ID-JAGs expire within five +minutes; access tokens issued by your authorization server follow your own expiry +and revocation policy.