Feat/w3ds OIDC bridge - #1102
Draft
Bekiboo wants to merge 7 commits into
Draft
Conversation
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description of change
GitW3 accepts W3DS login through a new OIDC bridge service. Forgejo believes it is talking to an
ordinary OpenID Connect provider; the wallet believes it is talking to an ordinary W3DS platform.
Neither is modified — which is what keeps the fork's patch surface at zero, as agreed in #1096.
Issue Number
Closes #1097
Type of change
How the change has been tested
Automated — 184 tests across 7 files (
pnpm --filter w3ds-oidc-bridge test), including a fullharness that drives
/authorize→ wallet callback →/token→/userinfoagainst a runninginstance. Also run locally:
build,check,check-format,check-lint,check-types. All green.End to end, locally, with real cryptography — a real eVault provisioned against a local
Registry, a real P-256 signature verified against it, and a GitW3 account created and reused:
W3DS_MIN_WALLET_VERSIONreports the reason on the page instead of hanging;than panicking — this is the path that breaks if the username claim is omitted instead of
emitted empty.
Acceptance criteria 1–4 are met. Criterion 5 — staging — is not. There is no service
deployment path in this repository to build on:
build.ymlbuilds and tests,docusaurus.ymlpublishes docs to Pages, and
docker-compose.databases.ymlruns local databases only. @coodos istaking the staging environment; the manifest in this PR is a candidate for it, clearly labelled as
a proposal rather than the house convention. I'd suggest keeping #1097 open until the flow has been
run in staging with a real wallet.
Change checklist
Design decisions
A bridge, not a patch. Forgejo has no plugin API, and #1096 committed us to a fork whose diff
against upstream stays empty so security releases merge cleanly. Teaching Forgejo about W3DS would
have cost both. Instead the bridge speaks a protocol Forgejo already implements. This works because
OIDC never specifies how a provider authenticates someone — here it is a QR code signed by an eID
wallet.
A minimal OIDC subset, hand-rolled on
jose. Only whatmarkbates/gothactually exercises:authorization code + PKCE S256, one client, one signing key. A general-purpose provider library
would have brought a data store, client registration and a dozen grant types we would never use.
ACCOUNT_LINKING = login, neverauto. Onauto, two eNames that sanitise to the sameusername would let the second person into the first person's account.
The username claim is always present, sometimes empty. Never absent. Forgejo's auto-registration
path guards the nil (
oauth.go:1120) but its link-account page does not (linkaccount.go:53, and anunguarded type assertion at
auth.go:405), so omitting the claim panics the fallback path — the onereached exactly when something has already gone wrong. There is a regression test asserting key
presence, not truthiness.
CORS on the W3DS half only. A native wallet sends no
Origin, but a browser-based one — the DevSandbox, which is the documented way to test this — triggers a preflight, and Express answers
OPTIONSwith a bare 200 and no headers. The browser then reports only "Failed to fetch". The OIDChalf never needs it: those are back-channel calls and top-level navigations.
Silent authentication is refused. After a logout Forgejo re-enters with
prompt=nonevia itslong-term SSO token. The bridge answers
login_requiredrather than rendering a QR page nobody islooking at. Only reproducible on the second login, which is why it is worth stating.
Synthetic email on a
.invaliddomain. W3DS carries no email address and Forgejo requires one.RFC 2606 reserves
.invalid, so these can never be delivered to a domain someone might register.They bounce by design — which is exactly why
REGISTER_EMAIL_CONFIRMmatters below.Trust model
gothnever verifies the ID token signature (openidConnect.go). TLS on the back channel plusthe client secret are therefore the only things separating a real ID token from a forged one. This
is why HTTPS is enforced by the service rather than left to the deployment, and why the unsafe case
has to be opted into with a named variable instead of inherited from a missing one.
The signature the bridge does verify is the wallet's: an ECDSA P-256 signature over the session
id, checked against the Registry through
@metastate-foundation/auth.What's in the PR
services/w3ds-oidc-bridge/— the service. Forgejo-facing:/.well-known/openid-configuration,/authorize(opens a session, serves the QR page),/token,/userinfo,/jwks,/icon.svg.Wallet-facing:
POST /w3ds/callbackandGET /w3ds/events/:session(SSE, so the browser knows whento continue), plus
/apple-touch-icon.png— the wallet resolves the app icon from the hostname ofthe redirect URI, so serving that path is what stops GitW3 appearing as a bare letter on the
approval screen.
docker/Dockerfile.w3ds-oidc-bridge— multi-stage,turbo prune, healthcheck on/healthz.docker-compose.gitw3.yml+docker/gitw3-register-auth-source.sh— a candidate deployment.The script exists because Forgejo keeps authentication sources in its database, not in
app.ini, so they cannot be declared with the rest of the configuration; without it a freshinstance needs someone to click through Site Administration before anyone can log in. It is
idempotent and safe to run on every deploy.
docs/superpowers/specs/anddocs/superpowers/plans/— the design and the implementation plan,including the open questions the staging owner needs to answer.
The
[oauth2_client]block, and the one that locks people outREGISTER_EMAIL_CONFIRMmust be set in this section. It otherwise inherits[service](
modules/setting/oauth2.go:69), and combined with a.invalidaddress every W3DS account iscreated inactive with its activation mail sent somewhere that never delivers — permanently
unactivatable, with no error anywhere. The local default is
falseon both sides, so this is theone that will not surface until staging.
docker-compose.gitw3.ymladditionally setsALLOW_ONLY_EXTERNAL_REGISTRATION = truewithDISABLE_REGISTRATION = false, which makes W3DS the only way in while leaving the link-account pageopen.
DISABLE_REGISTRATION = truewould close both and turn a rare edge case into a lockout.Why the bridge must start first
Forgejo fetches the discovery document once, while registering its authentication sources at
startup. If the bridge is down at that moment the source is not registered at all and the button
vanishes from the login page — with a misleading follow-on symptom, because an unregistered source
also stops Forgejo sending PKCE, so the bridge then rejects the request with
code_challenge is required. Once registered, the source survives a bridge restart. The composefile gates GitW3 on the bridge's healthcheck.