Add oauthproxy skeleton for AUTH-011 OAuth gradual rollout
What does this MR do and why?
Adds the Workhorse-side scaffolding for routing OAuth requests between GitLab Rails (Doorkeeper) and the new IAM Auth service during the AUTH-011 gradual rollout.
This MR ships only the wiring. No routing decisions are made yet — every OAuth request still falls through to Rails. The new oauthproxy package establishes a single decision point so that subsequent changes can add per-request routing rules without re-architecting the seam: one change for token-prefix routing on the token endpoints, and another for feature-flag-gated routing on the authorize endpoints.
What's in this MR
- New
--iamServiceURLCLI flag andIAM_SERVICE_URLenv-var fallback +Config.IAMServiceURL *url.URLfield. When neither is set, the new handler returns the existing Rails proxy unchanged (zero per-request overhead, byte-identical behavior to today). CLI flag wins when both are set. The env-var name follows the convention from auth-architecture/sandbox-config!27, where the same name was first introduced. A follow-up sandbox MR will re-add it toglobal.extraEnvso cloud-native deployments can inject it without templating a CLI arg. - New
internal/oauthproxypackage with aHandlerstruct +Build()method:Build()returns the bare Rails proxy whenIAMServiceURL == nil- Otherwise returns a wrapping handler that emits a structured log line per request and forwards to Rails
routes.gowires the 5 OAuth paths through the new handler:/oauth/authorize,/oauth/authorize_device,/oauth/token,/oauth/revoke,/oauth/introspect- Unit tests covering both
Build()branches, the env-var fallback, the flag-wins precedence, and the invalid-URL error path
What's intentionally NOT here
| Out of scope | Why |
|---|---|
| Real routing rules | Added in two follow-up MRs (token-prefix routing for the token endpoints; FF-gated routing for the authorize endpoints) |
/oauth/userinfo, /oauth/device |
AUTH-011: always Rails — handled by omission |
/.well-known/openid-configuration, /oauth/discovery/keys |
AUTH-011 Phase 1 handles these via a Cloudflare Origin Rule, not Workhorse |
| Outbound proxying to IAM | No IAMProxy field yet — added when the routing-rule follow-ups need it |
How to test this change
The MR is pure wiring; the only observable signal is structured log lines. CI exercises the unit tests; manual verification confirms the seam behaves correctly in a running GDK.
Setup
# Build workhorse from this branch
cd gitlab/workhorse
make
# Restart workhorse in GDK
gdk restart gitlab-workhorseTest case A — default (flag and env var unset, production-equivalent)
Don't set --iamServiceURL or IAM_SERVICE_URL. Tail the log:
tail -f log/gitlab-workhorse/current | grep oauthproxyIn another shell, hit any OAuth path through Workhorse's TCP listener:
curl -sk -X POST https://gdk.test:3333/oauth/token -d 'x=y' -o /dev/null -w '%{http_code}\n'Expected:
- At boot: one line
oauthproxy: IAMServiceURL not set; OAuth requests will be handled by Rails - Per request: zero
oauthproxy: routing decisionlines (thenilbranch returns the bare proxy with no wrapping) - The HTTP response code is whatever Rails/Doorkeeper returns — irrelevant; we're testing the seam
Test case B — observability mode (flag or env var set)
Edit services/gitlab-workhorse/run and append -iamServiceURL "http://gdk.test:8084" to the exec line. Restart:
sv restart services/gitlab-workhorseAlternative (env-var): instead of editing services/gitlab-workhorse/run, just export the var before the restart:
export IAM_SERVICE_URL="http://gdk.test:8084"
sv restart services/gitlab-workhorseSame observable behavior — confirms the env-var path is wired through the same code as the CLI flag.
Tail the log and curl all 5 wired paths:
for p in /oauth/authorize /oauth/authorize_device /oauth/token /oauth/revoke /oauth/introspect; do
curl -sk -X POST "https://gdk.test:3333$p" -o /dev/null -w '%{http_code}\n'
doneExpected:
- At boot:
oauthproxy: OAuth routing to IAM service enabled iam_backend_url=http://gdk.test:8084 - Per request: exactly one structured
oauthproxy: routing decisionline each, fields:method=POSTpath=/oauth/...destination=railsreason=rule_not_implementedcorrelation_id=...
- Total 5 routing-decision lines after the loop
Test case C — negative test (non-wired paths produce no log lines)
curl -sk https://gdk.test:3443/oauth/userinfo -o /dev/null
curl -sk https://gdk.test:3443/.well-known/openid-configuration -o /dev/nullExpected: no oauthproxy: routing decision lines — confirms routes.go correctly excluded these paths per AUTH-011 design.
Test case D — invalid URL fails fast
Set -iamServiceURL not-a-valid-url (or IAM_SERVICE_URL=not-a-valid-url) and restart workhorse.
Expected: workhorse refuses to boot with iamServiceURL: parse ...: invalid URI for request — confirms parseAuthBackend validates and misconfiguration is caught at startup via either configuration surface.
How the API endpoint from !237242 (merged) will be consumed here
This MR's Handler.API *api.API field is wired in now but currently unused. The follow-up that adds feature-flag-gated routing for /oauth/authorize* will use it to call the PreAuthorize endpoint shipped in !237242 (merged):
1. Browser: GET /oauth/authorize?client_id=acme&...
↓ hits Workhorse :3333
2. oauthproxy.Handler (this MR's seam):
- Extracts client_id from query
- Calls h.API.<method>(client_id) → POSTs to:
/api/v4/internal/workhorse/oauth_routing
3. Rails endpoint from !237242 (lib/api/internal/workhorse.rb):
- Looks up Authn::OauthApplication by_uid("acme")
- Checks Feature.enabled?(:proxy_oauth_requests_to_iam_service, application)
- Returns: {"destination": "iam"} or "rails"
4. oauthproxy.Handler:
- Forwards to IAM service URL or RailsProxy based on responseOnly /oauth/authorize and /oauth/authorize_device use this PreAuthorize call. The token endpoints (/oauth/token, /oauth/revoke, /oauth/introspect) decide locally by inspecting the token prefix (giat_ / ory_) — no Rails call needed for those.
Where this fits and what's next
The work on #594504 (closed) is being delivered as a sequence of focused MRs:
| Status | Change |
|---|---|
Add proxy_oauth_requests_to_iam_service feature flag + register Authn::OauthApplication as a FF actor |
|
Add POST /api/v4/internal/workhorse/oauth_routing PreAuthorize endpoint |
|
| This MR | Workhorse scaffolding + observability (no routing decisions yet) |
| Next | Token-prefix routing in Workhorse for /oauth/token, /oauth/revoke, /oauth/introspect |
| After that | FF-gated routing in Workhorse for /oauth/authorize*, consuming the !237242 (merged) endpoint |
Plus deployment configuration. For the sandbox (immediate need), a follow-up MR to auth-architecture/sandbox-config will re-add IAM_SERVICE_URL to global.extraEnv — the name follows the pattern Daniele established in !27; no chart change required. For long-term Omnibus / Charts / GDK templating with first-class operator-facing config keys, a follow-up issue tracks the work before broader rollout.
References
- Issue: #594504 (closed)
- Design: AUTH-011
- Rails-side PreAuthorize endpoint: !237242 (merged)
- Reference PoC (won't merge): !230496 (closed)