Add OAuth routing PreAuthorize endpoint for Workhorse
What does this MR do and why?
This is next MR in series where we add OAuth routing PreAuthorize endpoint for Workhorse as per our discussion here - #594504 (comment 3351311131) , we introduced FF as per our last MR here - !236263 (merged)
Workhorse calls this per OAuth request to decide whether to proxy the request to the IAM Auth Service or pass through to Rails, based on the proxy_oauth_requests_to_iam_service feature flag for the OAuth application identified by the request's client_id.
The endpoint lives next to authorize_upload in API::Internal::Workhorse and reuses the same JWT auth and content type conventions.
If client_id is missing or doesn't resolve to a known OAuth application, the per-application FF check doesn't apply; the endpoint returns "rails" as a documented fallback so Doorkeeper continues to own invalid_client error responses (RFC 6749 section 5.2).
Issue: #594504 (closed)
How to set up and validate locally
This endpoint is called by Workhorse and returns the application/vnd.gitlab-workhorse+json content-type. Workhorse's api.blocker prevents that content-type from being returned to external clients, so the endpoint cannot be tested through the normal https://gdk.test:3443 ingress — Workhorse rewrites the response to a 500. The endpoint must be exercised against Rails directly via its Unix socket.
Setup
-
In
gdk rails console, generate a Workhorse JWT and an OAuth application:require 'jwt' puts JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') org = Organizations::Organization.default_organization app = ::Authn::OauthApplication.create!( name: 'iam-routing-test', redirect_uri: 'https://example.com/cb', scopes: 'api', organization: org ) puts app.uid -
Export the values in your shell (adjust socket path to your GDK):
export SOCKET=/path/to/gitlab-development-kit/gitlab.socket export WH_JWT='<paste-jwt>' export CLIENT_ID='<paste-uid>'
Scenarios
1. Missing Workhorse JWT — rejected with 403
curl -s --unix-socket "$SOCKET" -X POST \
-H "Host: gdk.test" \
-d "client_id=$CLIENT_ID" \
-w '\nHTTP %{http_code}\n' \
http://localhost/api/v4/internal/workhorse/oauth_routingExpected: HTTP 403.
2. JWT present, no client_id — falls back to Rails
curl -s --unix-socket "$SOCKET" -X POST \
-H "Host: gdk.test" \
-H "Gitlab-Workhorse-Api-Request: $WH_JWT" \
-w '\nHTTP %{http_code}\n' \
http://localhost/api/v4/internal/workhorse/oauth_routingExpected: {"destination":"rails"} + HTTP 200.
3. JWT present, unknown client_id — falls back to Rails
curl -s --unix-socket "$SOCKET" -X POST \
-H "Host: gdk.test" \
-H "Gitlab-Workhorse-Api-Request: $WH_JWT" \
-d "client_id=nonexistent-uid" \
-w '\nHTTP %{http_code}\n' \
http://localhost/api/v4/internal/workhorse/oauth_routingExpected: {"destination":"rails"} + HTTP 200.
4. JWT present, known client_id, feature flag disabled — routes to Rails
In gdk rails console:
Feature.disable(:proxy_oauth_requests_to_iam_service)curl -s --unix-socket "$SOCKET" -X POST \
-H "Host: gdk.test" \
-H "Gitlab-Workhorse-Api-Request: $WH_JWT" \
-d "client_id=$CLIENT_ID" \
-w '\nHTTP %{http_code}\n' \
http://localhost/api/v4/internal/workhorse/oauth_routingExpected: {"destination":"rails"} + HTTP 200.
5. JWT present, known client_id, feature flag enabled for that app — routes to IAM
In gdk rails console:
app = ::Authn::OauthApplication.by_uid('<paste-CLIENT_ID>')
Feature.enable(:proxy_oauth_requests_to_iam_service, app)curl -s --unix-socket "$SOCKET" -X POST \
-H "Host: gdk.test" \
-H "Gitlab-Workhorse-Api-Request: $WH_JWT" \
-d "client_id=$CLIENT_ID" \
-w '\nHTTP %{http_code}\n' \
http://localhost/api/v4/internal/workhorse/oauth_routingExpected: {"destination":"iam"} + HTTP 200.
Notes for reviewers
- The Unix-socket call bypasses Workhorse entirely and exercises Rails directly. This is the correct integration boundary to validate for this MR — the consumer (Workhorse Go MR) is shipped separately.
- End-to-end validation through Workhorse and IAM is gated on the consumer MR landing.
- The request spec (
spec/requests/api/internal/workhorse_spec.rb) covers all five scenarios above plus content-type assertion.