Draft: Distributed OAuth Applications across Cells
Related MRs
| 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/tokenrequests 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.
Signed cookie for OAuth redirect storage
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 theMetadatafor an OAuth application claimfind_login_cell_prefix(login:)— returns thesession_prefixof 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