Skip to content

docs(sso): the OIDC flow uses /sign-in/social and /callback/:id - #21926

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-21885-sso-oidc-route
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-21885-sso-oidc-route

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Closes #21885

Clause-②: no

What changes

content/docs/permissions/sso.mdx, the "OAuth flow" section only. Step 2 of the OIDC flow told readers to call POST /api/v1/auth/sign-in/oauth2 with { providerId }. Nothing registers that route, so a reader following the page got a 404. The OIDC steps now use the two routes the social flow already uses:

  • Step 2: POST /api/v1/auth/sign-in/social with { provider: "okta", callbackURL }, where provider is the oidcProviders entry's providerId.
  • Step 4: the provider redirects to /api/v1/auth/callback/okta. better-auth exchanges the code, reads the profile from the ID token or userInfoUrl, creates a session and redirects to callbackURL.
  • A one-line lead-in says why: better-auth's generic-OAuth plugin registers each oidcProviders entry as a social provider and adds no endpoints of its own.

No product change, no other page, no changeset (docs do not publish).

Grounding for each route

POST /api/v1/auth/sign-in/social (OIDC step 2, changed; social step 2, re-checked and unchanged)

  • Ledger row, packages/plugins/plugin-auth/src/auth-route-ledger.ts line 167:
    { route: 'POST /api/v1/auth/sign-in/social', family: 'core-auth', source: 'better-auth', disposition: 'sdk', client: 'auth.signInWithProvider' },
  • Published-route list, same file, line 433: 'POST /api/v1/auth/sign-in/social',
  • Body shape, installed better-auth 1.7.3, dist/api/routes/sign-in.mjs line 40: provider: SocialProviderListEnum. In @better-auth/core dist/social-providers/index.mjs line 78 that is z.enum(socialProviderList).or(z.string()), so a generic provider id is accepted. The handler looks c.body.provider up in c.context.socialProviders.
  • The generic-OAuth plugin, dist/plugins/generic-oauth/index.mjs lines 61-66: "registers any OAuth/OIDC provider as a first-class social provider. Providers are used through the standard signIn.social and callback/:id core endpoints — no plugin-specific endpoints needed." Its init prepends the generic providers to ctx.socialProviders (line 272).
  • Tests merged with fix(plugin-auth)!: implicit account linking requires the standard local-ownership condition; unlink is honoured #21872, packages/plugins/plugin-auth/src/implicit-account-linking.test.ts:
    • lines 10-11: "driving a real OAuth round trip (/sign-in/social → /callback/:id) through generic-OAuth providers"
    • line 246: { provider: providerId, callbackURL: AFTER, disableRedirect: true, ...extraBody }, posted to sign-in/social

/api/v1/auth/callback/:id (OIDC step 4, now named; social step 4 /callback/google, re-checked and unchanged)

  • Published-route list, auth-route-ledger.ts line 321 'GET /api/v1/auth/callback/:id', and line 372 'POST /api/v1/auth/callback/:id',
  • Callback path, better-auth 1.7.3 dist/oauth2/utils.mjs line 29: if (!provider.callbackPath) return the path /callback/ plus provider.id. The generic-OAuth plugin sets no callbackPath, and auth-manager.ts passes providerId: p.providerId straight through (line 3710).
  • Test, implicit-account-linking.test.ts line 254: the callback is requested at ${BASE}/api/v1/auth/callback/${providerId}?code=code-1&state=….

POST /api/v1/auth/sign-in/oauth2 (removed)

  • grep -c "sign-in/oauth2" packages/plugins/plugin-auth/src/auth-route-ledger.ts gives 0, in both the ledger rows and the published-route list.

The section names no other route.

Validation

All at head 57258e13b4. node scripts/pm/dispatch-gates.mjs derived 44 gate commands for this one-file change. Every one of them exited 0, and --ran reconciled them: "44 derived famil(ies) accounted for — 44 run, 0 NOT-MEASURED (a DERIVED zero — all 44 recorded an exit code and none of them is 3)".

  • Docs gates that read this page: check:doc-authoring, check:doc-anchors (429 fragment links resolve), check-doc-route-spelling --advisory ("every shape-matched literal spells its ledger row"), check:docs-single-h1, check:doc-frontmatter, check:docs-redirects, check:docs-transcript-drift, check:nul-bytes. All passed.
  • Four gates first exited 3 (PREREQUISITE NOT MET, nothing measured): check:doc-formula-expressions, check:doc-security-posture, check:docs-transcript-drift and check:skill-examples. I built @objectstack/lint..., @objectstack/spec and @objectstack/client-react..., then re-ran all four. Each exited 0.

Acceptance notes

  • Outside this section and untouched: the SAML flow names POST /api/v1/auth/sign-in/sso. The ledger's pinned config (LEDGERED_PLUGIN_CONFIG) does not turn on sso, so the ledger neither confirms nor refutes that route. Noting it here only.

Changes 1 file: content/docs/permissions/sso.mdx, +8 / -3.


Generated by Claude Code

The OAuth flow section told readers to call POST /api/v1/auth/sign-in/oauth2,
a route nothing registers. better-auth's generic-OAuth plugin, which carries
the oidcProviders entries, adds no endpoints: it registers each entry as a
social provider, so sign-in starts at /sign-in/social with the entry's
providerId as `provider` and returns through /callback/:id.

Claude-Session: https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 57258e13b480ed625c756a3291c39599158a27eb
Local-runs: none

① Changesets — TRUE

  • git diff --name-only origin/main...57258e13 lists exactly one file, content/docs/permissions/sso.mdx (+8/-3); no .changeset/* file is added or changed by the PR.
  • The file is hand-written documentation under content/docs/, outside every published package, so the PR releases nothing: skip-changeset is the right disposition.
  • The PR carries labels documentation, size/s only; skip-changeset is not yet applied, so the Check Changeset gate (.github/workflows/pr-automation.yml:289, runs iff the label is absent) is red with the annotation "This PR adds no changeset ... if it releases nothing ... apply the 'skip-changeset' label". That is the expected state for a docs-only PR awaiting the label, not a defect in the diff (review is read-only; no label written).

② Review faces — TRUE

  • Route ledger (packages/plugins/plugin-auth/src/auth-route-ledger.ts, identical on origin/main and the PR head): POST /api/v1/auth/sign-in/social is a disposition row at line 167 (family: 'core-auth', source: 'better-auth', disposition: 'sdk', client: 'auth.signInWithProvider') and a published-surface entry at line 433; GET /api/v1/auth/callback/:id at line 321 and POST /api/v1/auth/callback/:id at line 372 are in the published list. No sign-in/oauth2 row exists anywhere in the file (grep: zero hits; the only oauth2/* rows are the oauth-provider family, lines 173-178, 312-384). The list is checked for exact equality against the live auth.api enumeration (ledger comment, lines 303-306), so the old POST /api/v1/auth/sign-in/oauth2 route the doc named is not mounted.
  • better-auth version: packages/plugins/plugin-auth/package.json:40 pins "better-auth": "1.7.3"; pnpm-lock.yaml:5740 resolves better-auth@1.7.3. node_modules is absent in this checkout, so the published 1.7.3 tarball was fetched from the npm registry into the scratchpad and read directly (package.json "version": "1.7.3").
    • dist/plugins/generic-oauth/index.mjs:61-66 (plugin docblock): "registers any OAuth/OIDC provider as a first-class social provider. Providers are used through the standard signIn.social and callback/:id core endpoints — no plugin-specific endpoints needed." The returned plugin object (lines 270-276) has only id, init, options, $ERROR_CODES; there is no endpoints key (grep for endpoints hits only the comment at line 66). init returns { context: { socialProviders: genericProviders.concat(ctx.socialProviders) } } (line 272) with each provider's id: c.providerId (line 137).
    • Core dist/api/routes/sign-in.mjs:116 defines /sign-in/social and resolves the provider by c.body.provider against c.context.socialProviders (line 148); dist/api/routes/callback.mjs:26 defines /callback/:id and resolves by c.params.id (lines 61, 92). So provider = providerId and the callback path is /callback/<providerId>; under the plugin's /api/v1/auth base path that is /api/v1/auth/callback/okta.
    • Prose "exchanges the code, reads the user's profile from the ID token or userInfoUrl, creates a session and redirects to callbackURL": callback.mjs:110 calls provider.validateAuthorizationCode, :122 provider.getUserInfo, :2 imports setSessionCookie, :203-205 redirects to callbackURL. The generic-oauth getUserInfo (index.mjs:219-227) calls fetchUserInfo (:37-60), which decodes the ID token when it carries sub and email, otherwise GETs userInfoUrl (configured or discovered, :98). Not overstated; the only elided nuance is an optional custom getUserInfo override, which the doc's field table does not expose.
  • Repo wiring: packages/plugins/plugin-auth/src/auth-manager.ts:3703-3709 passes config.oidcProviders to genericOAuth({ config: ... }) from better-auth/plugins/generic-oauth; :6766-6768 lists them under socialProviders in the public config, matching the doc's "type": "oidc" example.
  • Test merged by fix(plugin-auth)!: implicit account linking requires the standard local-ownership condition; unlink is honoured #21872 (packages/plugins/plugin-auth/src/implicit-account-linking.test.ts, identical on origin/main and the PR head): header comment lines 10-11 "driving a real OAuth round trip (/sign-in/social → /callback/:id) through generic-OAuth providers"; the round-trip helper posts { provider: providerId, callbackURL: AFTER, disableRedirect: true, ...extraBody } to sign-in/social (lines 239-246) and then requests ${BASE}/api/v1/auth/callback/${providerId}?code=...&state=... (line 254), with providerId taken from the generic-OAuth entry (lines 115-121, 134).
  • Section consistency: the social half (sso.mdx at head, lines 375-380) already used sign-in/social / callback/google; the OIDC half now uses the same two routes with provider: "okta" and /api/v1/auth/callback/okta (lines 382-391). The setup sections (lines 31-96) and "Important notes" (line 395, https://<your-domain>/api/v1/auth/callback/<provider-id>) agree. The remaining sign-in/sso at line 332 is the SSO-plugin (SAML/OIDC-by-domain) flow, a separate section, and is not touched. No other sign-in/oauth2 reference remains in the file or in packages/, content/, apps/.

③ Scope — TRUE

  • Diff is a single hunk at content/docs/permissions/sso.mdx lines 379-391, the "OAuth flow" section's OIDC half plus a two-sentence lead-in; no other section of the file and no product file, test, ledger, or workflow is touched (changed_files: 1 on the PR).
  • Head check-runs at review time (GET .../commits/57258e13.../check-runs, 31 runs): all completed runs are success or skipped except Check Changeset (failure, missing skip-changeset label, see ①); Test Core (1/6) and Lint & Repo Gates were still in_progress. Build Docs and Check Documentation Links are success.

Implemented-by: claude/issue-21885-sso-oidc-route
Reviewed-by: session_01VDtqoecgES7ScQYGbFVDRv

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 6, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 01:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 01:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit ce577ec Oct 6, 2026
37 of 38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21885-sso-oidc-route branch October 6, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(sso): the OIDC flow names a /sign-in/oauth2 endpoint that does not exist

2 participants