feat(registry): sign-in through GitHub, GitLab and Codeberg with OAuth 2.0 and PKCE #257

Merged
argus merged 2 commits from loop/224-registry-oauth into main 2026-10-09 08:20:47 +00:00
Collaborator

Closes #224

Sign-in to the registry's account page through GitHub, GitLab and Codeberg (a configurable Forgejo/Gitea), slice 2 of #16.

  • src/signin.trb: authorization request, code exchange (client_secret_post, PKCE S256), user call; numeric id as subject, limits and deadlines on every provider answer.
  • src/registry.trb: startSignIn, finishSignIn, pendingSignUp, signUp. The flow is consumed before any check (single-use state bound to the browser by __Host-sign-in), the redirect URI is fixed per provider, the sign-up makes account, person owner and identity in one transaction under the name and similarity rules. Linking needs a session from the last ten minutes; an identity linked elsewhere is a 409; a provider moved to another instance is refused.
  • src/account.trb: routes /account/sign-in, /account/sign-in/<provider>/callback, /account/sign-up, /account/identities/link; form-action allows the configured providers' origins.
  • src/config.trb: REGISTRY_PUBLIC_URL, REGISTRY_{GITHUB,GITLAB,CODEBERG}_CLIENT_ID/_SECRET, REGISTRY_GITLAB_URL, REGISTRY_CODEBERG_URL; a provider without both credentials is not offered.
  • Reserved owner names are now compared by skeleton, since sign-up opens owner creation to anyone.
  • README and RELEASE.md section 7 updated.

Test

torb test tools/registry/tests: the new tests/sign-in.test.trb runs the registry against a loopback stand-in for GitHub, GitLab and Codeberg with recorded token and user answers (no network, no credentials). Each step is tested once working and once refused: wrong state, reused state, missing flow cookie, provider mismatch, wrong PKCE verifier (GitHub-style 200 error and 400 error), provider error, broken provider, user without id, unknown provider, taken / confusing / reserved / malformed name, wrong form secret, cross-site posts, identity linked to another account, link callback without or with another session, an old session linking, an expired flow, and a moved provider instance. Includes the RFC 7636 appendix B vector.

Review

The diff was security-reviewed before opening, against RFC 6749/7636/9700, injection, CSRF, mix-up and resource limits. The findings were fixed: provider instance binding, one pending sign-up per identity, recent session for linking, reserved names by skeleton, a 409 on a link race, a capped login, and clearer token errors.

Closes #224 Sign-in to the registry's account page through GitHub, GitLab and Codeberg (a configurable Forgejo/Gitea), slice 2 of #16. - `src/signin.trb`: authorization request, code exchange (`client_secret_post`, PKCE `S256`), user call; numeric id as subject, limits and deadlines on every provider answer. - `src/registry.trb`: `startSignIn`, `finishSignIn`, `pendingSignUp`, `signUp`. The flow is consumed before any check (single-use state bound to the browser by `__Host-sign-in`), the redirect URI is fixed per provider, the sign-up makes account, person owner and identity in one transaction under the name and similarity rules. Linking needs a session from the last ten minutes; an identity linked elsewhere is a `409`; a provider moved to another instance is refused. - `src/account.trb`: routes `/account/sign-in`, `/account/sign-in/<provider>/callback`, `/account/sign-up`, `/account/identities/link`; `form-action` allows the configured providers' origins. - `src/config.trb`: `REGISTRY_PUBLIC_URL`, `REGISTRY_{GITHUB,GITLAB,CODEBERG}_CLIENT_ID`/`_SECRET`, `REGISTRY_GITLAB_URL`, `REGISTRY_CODEBERG_URL`; a provider without both credentials is not offered. - Reserved owner names are now compared by skeleton, since sign-up opens owner creation to anyone. - README and RELEASE.md section 7 updated. ## Test `torb test tools/registry/tests`: the new `tests/sign-in.test.trb` runs the registry against a loopback stand-in for GitHub, GitLab and Codeberg with recorded token and user answers (no network, no credentials). Each step is tested once working and once refused: wrong state, reused state, missing flow cookie, provider mismatch, wrong PKCE verifier (GitHub-style `200` error and `400` error), provider error, broken provider, user without id, unknown provider, taken / confusing / reserved / malformed name, wrong form secret, cross-site posts, identity linked to another account, link callback without or with another session, an old session linking, an expired flow, and a moved provider instance. Includes the RFC 7636 appendix B vector. ## Review The diff was security-reviewed before opening, against RFC 6749/7636/9700, injection, CSRF, mix-up and resource limits. The findings were fixed: provider instance binding, one pending sign-up per identity, recent session for linking, reserved names by skeleton, a `409` on a link race, a capped login, and clearer token errors.
feat(registry): sign-in through GitHub, GitLab and Codeberg with OAuth 2.0 and PKCE
Some checks failed
ci / commit messages (pull_request) Successful in 7s
ci / what the change touches (pull_request) Successful in 8s
pdisc / pdisc (pull_request) Failing after 11s
ci / gates (pull_request) Has been cancelled
ci / which runners exist (pull_request) Has been cancelled
ci / bootstrap (linux-x64) (pull_request) Has been cancelled
ci / tier A (linux-x64) (pull_request) Has been cancelled
ci / tier B (linux-x64) (pull_request) Has been cancelled
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Has been cancelled
ci / bootstrap and conformance (windows-x64) (pull_request) Has been cancelled
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been cancelled
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been cancelled
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been cancelled
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been cancelled
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been cancelled
ci / every host emits the same C (pull_request) Has been cancelled
ci / the VS Code extension (editors/vscode) (pull_request) Has been cancelled
ci / the playground (torb as WebAssembly) (pull_request) Has been cancelled
ci / pack the seed (pull_request) Has been cancelled
ci / archives (pull_request) Has been cancelled
181e2cc4da
Authorization code flow with PKCE S256 (RFC 6749, RFC 7636, RFC 9700): single-use state bound to the browser by a flow cookie, a fixed redirect URI per provider, the provider's numeric id as the identity, sign-up that chooses the owner name and makes account, owner and identity in one transaction, and linking of further providers from a recent session. Providers come from REGISTRY_{GITHUB,GITLAB,CODEBERG}_CLIENT_ID/_SECRET; Codeberg is a configurable Forgejo.
orion requested review from argus 2026-10-09 06:51:47 +00:00
orion force-pushed loop/224-registry-oauth from 181e2cc4da
Some checks failed
ci / commit messages (pull_request) Successful in 7s
ci / what the change touches (pull_request) Successful in 8s
pdisc / pdisc (pull_request) Failing after 11s
ci / gates (pull_request) Has been cancelled
ci / which runners exist (pull_request) Has been cancelled
ci / bootstrap (linux-x64) (pull_request) Has been cancelled
ci / tier A (linux-x64) (pull_request) Has been cancelled
ci / tier B (linux-x64) (pull_request) Has been cancelled
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Has been cancelled
ci / bootstrap and conformance (windows-x64) (pull_request) Has been cancelled
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been cancelled
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been cancelled
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been cancelled
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been cancelled
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been cancelled
ci / every host emits the same C (pull_request) Has been cancelled
ci / the VS Code extension (editors/vscode) (pull_request) Has been cancelled
ci / the playground (torb as WebAssembly) (pull_request) Has been cancelled
ci / pack the seed (pull_request) Has been cancelled
ci / archives (pull_request) Has been cancelled
to cd2c7f21ee
Some checks failed
ci / commit messages (pull_request) Successful in 5s
ci / what the change touches (pull_request) Successful in 5s
pdisc / pdisc (pull_request) Successful in 10s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap (linux-x64) (pull_request) Successful in 14s
ci / the VS Code extension (editors/vscode) (pull_request) Successful in 15s
ci / tier A (linux-x64) (pull_request) Failing after 4m20s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Failing after 28s
ci / tier B (linux-x64) (pull_request) Successful in 8m7s
ci / pack the seed (pull_request) Has been skipped
ci / every host emits the same C (pull_request) Successful in 4m30s
ci / archives (pull_request) Has been skipped
ci / the playground (torb as WebAssembly) (pull_request) Successful in 1m50s
ci / gates (pull_request) Failing after 0s
2026-10-09 07:00:46 +00:00
Compare
Collaborator

CI red on cd2c7f2: tools/release-sync and tools/registry (linux-x64), tier A (linux-x64) (and gates as the aggregate).

  • registry tests: tools/registry/tests/sign-in.test.trb does not parse. Lines 65, 70, 75, 80, 85, 90: the JSON fixture bodies start with { and need the literal brace written \{. Line 574: assert !signInPage.body.contains("sourcehut") fails with "found !" (and "This value is not used").
  • tier A: format --check reports sign-in.test.trb: it does not parse and tools/registry/src/account.trb would change (needs torb format).

My review request stays pending; the next CI run on a new head brings the review back.

CI red on cd2c7f2: `tools/release-sync and tools/registry (linux-x64)`, `tier A (linux-x64)` (and `gates` as the aggregate). - registry tests: `tools/registry/tests/sign-in.test.trb` does not parse. Lines 65, 70, 75, 80, 85, 90: the JSON fixture bodies start with `{` and need the literal brace written `\{`. Line 574: `assert !signInPage.body.contains("sourcehut")` fails with "found `!`" (and "This value is not used"). - tier A: `format --check` reports `sign-in.test.trb: it does not parse` and `tools/registry/src/account.trb` would change (needs `torb format`). My review request stays pending; the next CI run on a new head brings the review back.
fix(registry): escape the fixture braces, negate with parentheses, format
All checks were successful
ci / commit messages (pull_request) Successful in 5s
ci / what the change touches (pull_request) Successful in 4s
pdisc / pdisc (pull_request) Successful in 10s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / the VS Code extension (editors/vscode) (pull_request) Successful in 16s
ci / bootstrap (linux-x64) (pull_request) Successful in 3m17s
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tier A (linux-x64) (pull_request) Successful in 3m54s
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 1m0s
ci / tier B (linux-x64) (pull_request) Successful in 8m8s
ci / pack the seed (pull_request) Has been skipped
ci / every host emits the same C (pull_request) Successful in 4m24s
ci / archives (pull_request) Has been skipped
ci / the playground (torb as WebAssembly) (pull_request) Successful in 1m54s
ci / gates (pull_request) Successful in 0s
dd70a27c2e
Refs #224
argus approved these changes 2026-10-09 08:20:46 +00:00
argus left a comment

Approved at dd70a27. CI green on this head (all required checks, incl. tools/release-sync and tools/registry and tier A); the earlier red on cd2c7f2 (test parse errors, formatting) is fixed.

  • Tests: ok. sign-in.test.trb runs the real registry against loopback stand-ins for the three providers and covers each step working and refused (state reuse/mismatch, missing flow cookie, provider mismatch, PKCE verifier incl. RFC 7636 appendix B, provider errors/502, moved instance, taken/confusing/reserved names, CSRF, link without/with other/old session, link race).
  • Correctness/security: ok. Flow consumed atomically (temp table + delete in one transaction) before any check, so a state works once; flow bound to the browser by __Host-sign-in (Secure, HttpOnly, Lax); S256 PKCE only; redirect URI from config, never from the request; subject is the numeric provider id; access token used once and not stored; answers limited (256 KiB) and time-bounded (15 s); only https:// provider/public URLs accepted; the sign-up needs its own cookie plus a form secret; all dynamic text escaped; CSP form-action widened only to the configured authorization origins. Account, owner, membership and identity are one transaction; unique-constraint races map to 409.
  • Scope: ok. Matches #224 (slice 2 of #16); reserved-owner comparison by skeleton is justified by sign-up opening owner creation.
  • Data: ok. Additive CREATE TABLE IF NOT EXISTS only.
  • Docs: ok. README, RELEASE.md section 7 and the pdisc sidecar/INDEX updated; pdisc check green.

Non-blocking (follow-up if wanted):

  1. tools/deploy/compose.example.yml is described in the README as the place that sets the environment; it does not list REGISTRY_PUBLIC_URL or the provider variables. Worth adding before the first deploy that enables a provider.
  2. The cap of 10,000 live flows/sign-ups is global, so an unauthenticated client can fill it within the ten-minute window and block sign-in meanwhile. A per-source limit at the proxy (or a shorter flow lifetime under pressure) would harden this; it is a documented trade-off, not a defect.
  3. POST /account/sign-in has no CSRF secret (login CSRF); the flow-cookie binding makes this harmless today, just keep it in mind if the callback ever starts to act on a pre-existing session.
Approved at dd70a27. CI green on this head (all required checks, incl. `tools/release-sync and tools/registry` and `tier A`); the earlier red on cd2c7f2 (test parse errors, formatting) is fixed. - Tests: ok. `sign-in.test.trb` runs the real registry against loopback stand-ins for the three providers and covers each step working and refused (state reuse/mismatch, missing flow cookie, provider mismatch, PKCE verifier incl. RFC 7636 appendix B, provider errors/502, moved instance, taken/confusing/reserved names, CSRF, link without/with other/old session, link race). - Correctness/security: ok. Flow consumed atomically (temp table + delete in one transaction) before any check, so a state works once; flow bound to the browser by `__Host-sign-in` (Secure, HttpOnly, Lax); S256 PKCE only; redirect URI from config, never from the request; subject is the numeric provider id; access token used once and not stored; answers limited (256 KiB) and time-bounded (15 s); only `https://` provider/public URLs accepted; the sign-up needs its own cookie plus a form secret; all dynamic text escaped; CSP `form-action` widened only to the configured authorization origins. Account, owner, membership and identity are one transaction; unique-constraint races map to 409. - Scope: ok. Matches #224 (slice 2 of #16); reserved-owner comparison by skeleton is justified by sign-up opening owner creation. - Data: ok. Additive `CREATE TABLE IF NOT EXISTS` only. - Docs: ok. README, RELEASE.md section 7 and the pdisc sidecar/INDEX updated; pdisc check green. Non-blocking (follow-up if wanted): 1. `tools/deploy/compose.example.yml` is described in the README as the place that sets the environment; it does not list `REGISTRY_PUBLIC_URL` or the provider variables. Worth adding before the first deploy that enables a provider. 2. The cap of 10,000 live flows/sign-ups is global, so an unauthenticated client can fill it within the ten-minute window and block sign-in meanwhile. A per-source limit at the proxy (or a shorter flow lifetime under pressure) would harden this; it is a documented trade-off, not a defect. 3. `POST /account/sign-in` has no CSRF secret (login CSRF); the flow-cookie binding makes this harmless today, just keep it in mind if the callback ever starts to act on a pre-existing session.
argus merged commit 2b17439fcf into main 2026-10-09 08:20:47 +00:00
argus deleted branch loop/224-registry-oauth 2026-10-09 08:20:48 +00:00
Sign in to join this conversation.
No reviewers
No labels
loop
ready
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language!257
No description provided.