GitLab Rails: Serve /oauth/userinfo for IAM-issued JWTs
Related to #603630 (closed)
What does this MR do and why?
- IAM-issued access tokens are stateless JWTs with no database row, so Doorkeeper's lookup rejected them and
GET /oauth/userinforeturned 401 for valid IAM tokens. - Serves the endpoint from a GitLab-owned controller that resolves the bearer as an IAM JWT first and falls back to Doorkeeper. This is the same pattern as AuthFinders#find_oauth_access_token.
- The token flows through the gem's unchanged claims pipeline, so responses are identical to Doorkeeper-issued tokens (asserted in the request spec).
NB:
- Refresh tokens are broken and can't be used with this flow. gitlab-org/auth/iam!390 (diffs) is still in flight.
- Revocation is out of scope and is tracked in #593992.
Gating
- Existing user-scoped
iam_svc_oauthfeature flag plus the IAM service integration. With either disabled, behavior is unchanged.
Database Review
No migrations, no query changes.
Screenshots & Recordings
| before (iam) | after (doorkeeper-regression-check) | after (iam) |
|---|---|---|
How to set up and validate locally
Full setup is documented in the IAM project:
Note: The steps below use
https://gdk.test:3443(my GDK setup). Your GDK port may differ inconfig/gitlab.yml, so adjust the URLs to match your local configuration.
The condensed steps for this MR:
1. Run the IAM service locally
From a checkout of gitlab-org/auth/iam, following iam-service-local-setup.md:
-
Edit
configs/environments/development-l2.tomlso the service points at your GDK, in particular:[services.auth.oauth] issuer = "https://gdk.test" login_url = "https://gdk.test:3443/users/sign_in" consent_url = "https://gdk.test:3443/-/iam/consent" [services.auth.oauth_client] gitlab_url = "https://gdk.test:3443" -
Build and start it (Postgres via Docker is required):
make make up CONFIG_DIR=$(pwd)/configs bin/iam-service --serve all -
Verify:
curl http://gdk.test:8084/healthreturns{"status":"ok"}. -
If you use Chrome, apply the secure-cookie flag workaround now. Without it the authorization code flow in step 3 fails with
missing login csrf cookie.
2. Point GDK Rails at the IAM service
Following gitlab-rails-local-setup.md, which also enables the iam_svc_oauth feature flag:
-
Create the shared secret file:
mkdir -p "$HOME/.gdk/iam-auth" echo "dev-service-token-do-not-use-in-production" > "$HOME/.gdk/iam-auth/.gitlab_iam_auth_secret" -
Add the
iam_auth_servicesection togitlab/config/gitlab.ymlunderdevelopment:, just before# 4. Advanced settings(keys documented inconfig/gitlab.yml.example):iam_auth_service: enabled: true secret_file: /full/path/to/home/.gdk/iam-auth/.gitlab_iam_auth_secret http: host: "gdk.test" port: 8084 jwt_audience: "gitlab-rails" jwt_issuer: "https://gdk.test"The
secret_filemust be a full absolute path, as~is not expanded.jwt_issuermust match theissuerin the IAM TOML config andjwt_audiencethe token'saudclaim, otherwise JWT validation rejects the token. Note thatgdk reconfigureremoves this section, because IAM is not yet templated in GDK, so re-add it if needed. -
gdk restart
3. Register an OAuth client on both sides
- In GitLab at
https://gdk.test:3443/-/user_settings/applications, create an application with redirect URIhttps://gdk.test:3443, non-confidential, and scopes includingopenid. The/oauth/userinfoendpoint requires it, and a token without it gets 403. - Mirror the client on the IAM service with the
grpcurlCreateClientcall from the setup doc, using the client ID and secret from the previous step and including"openid"inscopes.
4. Obtain an IAM-issued JWT
Run the authorization code flow with PKCE against the IAM service, the same flow as in !232772 (merged):
-
Generate PKCE parameters:
export CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_') export CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=') -
Open the authorize URL in a browser, sign in, and consent:
http://gdk.test:8084/oauth2/authorize?client_id=<CLIENT_ID>&redirect_uri=https://gdk.test:3443&response_type=code&scope=openid&state=test&code_challenge=<CODE_CHALLENGE>&code_challenge_method=S256 -
The browser lands back on the redirect URI. The rendered page does not matter and can be a 404 unless that URI serves an HTML page. What matters is the
codequery parameter in the address bar. -
Exchange the
codefor a token. The client is non-confidential, so no client secret is sent andclient_idgoes in the body:curl -X POST http://gdk.test:8084/oauth2/token \ -d "grant_type=authorization_code&client_id=<CLIENT_ID>&code=<CODE>&redirect_uri=https://gdk.test:3443&code_verifier=${CODE_VERIFIER}"The
access_tokenin the response is the IAM JWT. The payload follows the standard token response ofaccess_token,token_type,expires_in,refresh_tokenandcreated_at, as documented in Authorization code with PKCE.
5. Validate
curl --header "Authorization: Bearer <IAM_JWT>" "https://gdk.test:3443/oauth/userinfo"Expect 200 with the same JSON a Doorkeeper token returns for the same user, where sub is the user ID. The claims are the standard set documented in Shared information.
Example response
{
"sub": "1",
"sub_legacy": "9256f5dfce3b7acb61096ce6ab8361a1f4b4de8549ce410a0f1e6ceff822c287",
"name": "Administrator",
"nickname": "root",
"preferred_username": "root",
"given_name": "Administrator",
"profile": "https://gdk.test:3443/root",
"picture": "https://secure.gravatar.com/avatar/0f4682b94dcf1c6b2e32dcf4f6069cbbc371e09c92e8cca7bd4c87c2b803b3c1?s=80&d=identicon",
"groups": [
"gitlab-org",
"group1",
"group1/group2",
"tanuki",
"tanuki/sub1",
"tanuki/sub1/tanuki-sub-2"
],
"https://gitlab.org/claims/groups/owner": [
"group1",
"group1/group2",
"tanuki",
"tanuki/sub1",
"tanuki/sub1/tanuki-sub-2"
],
"https://gitlab.org/claims/groups/developer": [
"gitlab-org"
]
}MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.