Draft: Distributed OAuth Applications across Cells

Repository MR
topology-service gitlab-org/cells/topology-service!518 (closed)
http-router gitlab-org/cells/http-router!1207 (closed)

Summary

Extends the OAuth/OpenID provider stack so that tokens, auth codes, and auth flows work across cells in a multi-cell GitLab deployment.

  • Routable OAuth tokens (gloak-, gloag-, gloar- prefixes) let the HTTP Router direct /oauth/token requests to the issuing cell
  • OAuth application data is published to the Topology Service so any cell can validate tokens for apps registered on another cell
  • The OAuth redirect URL is stored in a signed JWT cookie instead of a server-side session, so it survives cross-cell login redirects
  • The sign-in flow detects when a user's account lives on a different cell and routes them there automatically, including after two-factor authentication

Changes

Routable OAuth tokens

lib/authn/oauth_routable_token_generator.rb wraps Authn::TokenField::Generator::RoutableToken and emits tokens with a type-specific prefix:

Token type Prefix
Access token gloak-
Auth code gloag-
Refresh token gloar-

Doorkeeper::AccessToken overrides generate_token and generate_refresh_token; Doorkeeper::AccessGrant overrides generate_token. Both delegate to OauthRoutableTokenGenerator when Gitlab.config.cell.enabled, falling back to Doorkeeper::OAuth::Helpers::UniqueToken.generate otherwise.

config/initializers/doorkeeper.rb sets access_token_generator '::Authn::OauthRoutableTokenGenerator' as the Doorkeeper-level default, which is overridden per-model as above.

OAuth application realm and cross-cell lookup

A new realm column (integer, default cluster_global) is added to oauth_applications via migration 20260514000001. The enum maps to :organization_local and :cluster_global.

Authn::OauthApplication includes Cells::Claimable and publishes a JSON user_data blob to the Topology Service whenever any of secret, redirect_uri, scopes, name, confidential, realm, or organization_id changes. OauthApplication.by_uid overrides the Doorkeeper base to fall back to a Topology Service GetRecord lookup when the application is not found locally, hydrating an in-memory (non-persisted) record from the claim data.

lib/gitlab/cells/signed_cookie.rb encodes an arbitrary data hash into a short-lived RS256 JWT (10 min TTL, JTI replay protection). decode verifies the signature and JTI; peek decodes without consuming the JTI and returns the data hash.

app/controllers/concerns/cells/stored_redirect.rb overrides Devise's store_location_for / stored_location_for for the :redirect scope: when Cells is enabled, the return URL is written to and read from a _gitlab_sign_in_return cookie via SignedCookie instead of a plain session key. The JTI is consumed on first read, preventing replay.

Cells::StoredRedirect is included in both ApplicationController and Gitlab::BaseDoorkeeperController so all sign-in and OAuth authorization flows use the cookie path.

config/initializers/doorkeeper.rb resource_owner_authenticator now calls store_location_for(:redirect, request.fullpath) instead of writing directly to session[:user_return_to].

Cross-cell login routing

SessionsController#determine_sign_in_path checks whether the submitted login (username or email) belongs to a user on a different cell. If the user is not found locally, ClaimService#find_login_cell_prefix queries the Topology Service for the cell that owns that username or email. When a match is found, the controller sets a session-routing cookie and redirects the browser to the sign-in page with the login pre-filled as a query parameter.

app/helpers/sessions_helper.rb passes is_login_prefilled: @username_or_email.present? to the sign-in Vue app. app/assets/javascripts/authentication/sign_in/components/sign_in_form.vue makes the username field read-only when isLoginPrefilled is true, preventing the user from changing identity mid-flow.

JWKS aggregation

JwksController#load_keys fetches the PEM-encoded signing key from every cell via Gitlab::TopologyServiceClient::CellService#list_signing_keys and appends them to the local keys returned by the JWKS endpoint. This lets OIDC relying parties verify tokens issued by any cell with a single JWK Set request.

Topology Service client additions

Gitlab::TopologyServiceClient::ClaimService gains two public methods backed by a shared get_record_by_bucket helper (uses the GetRecord gRPC RPC, handles GRPC::NotFound):

  • find_oauth_application_data(uid:) — returns the Metadata for an OAuth application claim
  • find_login_cell_prefix(login:) — returns the session_prefix of the cell that owns a username or email

Vendored proto files (cell_info_pb.rb, cell_service_pb.rb, claims/v1/messages_pb.rb, classify_service_pb.rb, types/v1/claim_pb.rb) are updated to match Topology Service proto changes in the companion MR.

Test plan

  • bundle exec rspec spec/lib/gitlab/cells/signed_cookie_spec.rb
  • bundle exec rspec spec/controllers/concerns/cells/stored_redirect_spec.rb
  • bundle exec rspec spec/models/authn/oauth_application_spec.rb
  • bundle exec rspec spec/lib/gitlab/topology_service_client/claim_service_spec.rb
  • bundle exec rspec spec/lib/gitlab/topology_service_client/cell_service_spec.rb
  • bundle exec rspec spec/models/doorkeeper/access_token_spec.rb
  • bundle exec rspec spec/models/doorkeeper/access_grant_spec.rb
  • bundle exec rspec spec/lib/authn/oauth_routable_token_generator_spec.rb
  • bundle exec rspec spec/controllers/sessions_controller_spec.rb
  • bundle exec rspec spec/requests/jwks_controller_spec.rb
  • Manual: OAuth flow with user and app on different cells routes correctly end-to-end

🤖 Generated with GitLab Agentic Modular

Edited by Kamil Trzciński (Claude)

Merge request reports

Loading
Loading