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/userinfo returned 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:

Gating

  • Existing user-scoped iam_svc_oauth feature 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 in config/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:

  1. Edit configs/environments/development-l2.toml so 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"
  2. Build and start it (Postgres via Docker is required):

    make
    make up
    CONFIG_DIR=$(pwd)/configs bin/iam-service --serve all
  3. Verify: curl http://gdk.test:8084/health returns {"status":"ok"}.

  4. 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:

  1. 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"
  2. Add the iam_auth_service section to gitlab/config/gitlab.yml under development:, just before # 4. Advanced settings (keys documented in config/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_file must be a full absolute path, as ~ is not expanded. jwt_issuer must match the issuer in the IAM TOML config and jwt_audience the token's aud claim, otherwise JWT validation rejects the token. Note that gdk reconfigure removes this section, because IAM is not yet templated in GDK, so re-add it if needed.

  3. gdk restart

3. Register an OAuth client on both sides

  1. In GitLab at https://gdk.test:3443/-/user_settings/applications, create an application with redirect URI https://gdk.test:3443, non-confidential, and scopes including openid. The /oauth/userinfo endpoint requires it, and a token without it gets 403.
  2. Mirror the client on the IAM service with the grpcurl CreateClient call from the setup doc, using the client ID and secret from the previous step and including "openid" in scopes.

4. Obtain an IAM-issued JWT

Run the authorization code flow with PKCE against the IAM service, the same flow as in !232772 (merged):

  1. 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 '=')
  2. 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
  3. 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 code query parameter in the address bar.

  4. Exchange the code for a token. The client is non-confidential, so no client secret is sent and client_id goes 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_token in the response is the IAM JWT. The payload follows the standard token response of access_token, token_type, expires_in, refresh_token and created_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.

Edited by Hakeem Abdul-Razak

Merge request reports

Loading
Loading