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 --iamServiceURL CLI flag and IAM_SERVICE_URL env-var fallback + Config.IAMServiceURL *url.URL field. 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 to global.extraEnv so cloud-native deployments can inject it without templating a CLI arg.
  • New internal/oauthproxy package with a Handler struct + Build() method:
    • Build() returns the bare Rails proxy when IAMServiceURL == nil
    • Otherwise returns a wrapping handler that emits a structured log line per request and forwards to Rails
  • routes.go wires 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-workhorse

Test 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 oauthproxy

In 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 decision lines (the nil branch 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-workhorse

Alternative (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-workhorse

Same 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'
done

Expected:

  • At boot: oauthproxy: OAuth routing to IAM service enabled iam_backend_url=http://gdk.test:8084
  • Per request: exactly one structured oauthproxy: routing decision line each, fields:
    • method=POST
    • path=/oauth/...
    • destination=rails
    • reason=rule_not_implemented
    • correlation_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/null

Expected: 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 response

Only /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
Merged (!236263 (merged)) Add proxy_oauth_requests_to_iam_service feature flag + register Authn::OauthApplication as a FF actor
Merging (!237242 (merged)) 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

Edited by Smriti Garg

Merge request reports

Loading