Skip to content

feat(orb): maintainer OAuth self-enrollment (install-admin verified) - #1348

Merged
JSONbored merged 1 commit into
mainfrom
feat/orb-maintainer-self-enrollment
Jun 25, 2026
Merged

feat(orb): maintainer OAuth self-enrollment (install-admin verified)#1348
JSONbored merged 1 commit into
mainfrom
feat/orb-maintainer-self-enrollment

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Lets a maintainer self-issue their brokered enrollment secret via the Orb App's OAuth callback — without the operator manually issuing it — while closing the privilege-escalation hole that made the operator-issued path the only safe option. GitHub redirects to /v1/orb/oauth/callback with an OAuth code + installation_id; the handler now: exchanges the code (Orb App credentials) → identifies the user (GET /user) → verifies the user is an admin of the installation's account → checks registered=1 → issues a one-time secret, recording the maintainer identity.

The security gate (the whole point)

installation_id is an attacker-controllable query param, so a stolen code + a victim's installation_id must never enroll the victim's install. verifyInstallationAdmin:

  • Org install → the user must be an active org admin (their own GET /user/memberships/orgs/{org}role==="admin" && state==="active"; needs the read:org scope).
  • User install → the user must be the account owner (case-insensitive login match).
  • API error / non-OK / member / pending → false.

installation_id is bound server-side in the enrollment and read back (never from a request) at token-exchange. No request input is echoed into the markup; the secret is shown once and never logged. No migration — orb_enrollments already had the maintainer_* columns; the operator-issued path is unchanged (maintainer optional).

Validation

  • npm run test:ci green; 100% branch coverage on oauth.ts + broker.ts diff (every admin-verify arm: org-admin/member/pending/api-error, user-match/mismatch, no-account; the full callback flow incl. the non-admin 403 + no-enrollment gate, unregistered 403, unknown 404, exchange//user failure 400).
  • Adversarially verified — all 6 properties hold (cross-install enrollment, admin-verify correctness, token confusion, installation_id binding, registered gate, info-leak/XSS).

Safety

  • Closes the OAuth admin-of-installation privilege-escalation hole. No secret in logs/markup.

Advances #1255. Configure the Orb App OAuth scopes → read:user + read:org for the org-admin check.

Let a maintainer self-issue their brokered enrollment secret via the Orb App's OAuth callback — without the
operator manually issuing it — while CLOSING the privilege-escalation hole that made the operator-issued path the
only option. GitHub redirects to /v1/orb/oauth/callback with an OAuth code + installation_id; the handler now:
exchanges the code (Orb App credentials) → identifies the user (GET /user) → VERIFIES the user is an admin of the
installation's account → checks registered=1 → issues a one-time secret, recording the maintainer identity.

The admin check is the gate: installation_id is an attacker-controllable query param, so a stolen code + a
victim's installation_id must never enroll the victim's install. For an Org install the user must be an ACTIVE
org ADMIN (their own /user/memberships/orgs role, requires the read:org scope); for a User install they must be
the account owner. installation_id is bound server-side in the enrollment and read back (never from a request) at
token-exchange time. No request input is echoed into the markup; the secret is shown once and never logged.

issueOrbEnrollment now records the maintainer login + github id (the orb_enrollments columns already existed; no
migration). The operator-issued path is unchanged (maintainer optional). Adversarially verified (6 properties).

Advances #1255. (Configure the Orb App OAuth scopes to read:user + read:org for the org-admin check.)
@dosubot dosubot Bot added the size:L label Jun 25, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.28%. Comparing base (cc8dc3f) to head (5d96933).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1348   +/-   ##
=======================================
  Coverage   95.27%   95.28%           
=======================================
  Files         188      188           
  Lines       20336    20371   +35     
  Branches     7327     7343   +16     
=======================================
+ Hits        19375    19410   +35     
  Misses        378      378           
  Partials      583      583           
Files with missing lines Coverage Δ
src/orb/broker.ts 100.00% <100.00%> (ø)
src/orb/oauth.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JSONbored
JSONbored merged commit 1b3af12 into main Jun 25, 2026
18 checks passed
@JSONbored
JSONbored deleted the feat/orb-maintainer-self-enrollment branch June 25, 2026 09:58
JSONbored added a commit that referenced this pull request Jun 25, 2026
…1355)

Closes the brokered self-host loop (#1255): the container now self-registers its public relay URL with the
central Orb on startup, so the Orb forwards this install's events to it — no manual curl. The container computes
its relay URL from PUBLIC_API_ORIGIN + /v1/orb/relay and POSTs it to the broker with its enrollment secret.

registerOrbRelayTarget (src/orb/broker-client.ts) is BEST-EFFORT + fire-and-forget: skipped unless broker mode +
PUBLIC_API_ORIGIN are set, and any failure (Orb down, install not registered yet, non-public origin rejected by
the Orb's SSRF check) just means no relay until the next boot — it never throws or blocks startup. Wired into the
selfhost boot alongside the orb-export hook (server.ts, the codecov-ignored process entry).

End-to-end now: install Orb App → self-enroll (admin-verified, #1348) → broker tokens (#1341) → boot
auto-registers relay (this) → Orb forwards events (#1352) → relay receiver verifies + enqueues (#1354) → review +
act. Advances #1255.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GitHub App identity unification + atomic webhook-secret swap (retire reviewwed[bot])

1 participant