diff --git a/Gemfile b/Gemfile
index bdf30c699335394100a457b16b1c8d891c91b303..97e2c6f602120c61820119f10e298c6a333eeae1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -743,3 +743,5 @@ gem 'openbao_client', path: 'gems/openbao_client' # rubocop:todo Gemfile/Missing
 gem 'paper_trail', '~> 15.0' # rubocop:todo Gemfile/MissingFeatureCategory
 
 gem "i18n_data", "~> 0.13.1", feature_category: :system_access
+
+gem "gitlab-cloud-connector", "~> 0.2.1", require: 'cloud_connector', feature_category: :cloud_connector
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 9b6d34d1c5fb48156eda5661d7bbcf59443810d4..2ad02e36715c9bbc30529db75d7a629dcea9639b 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -220,6 +220,7 @@
 {"name":"gitaly","version":"17.5.0.pre.rc42","platform":"ruby","checksum":"15469230245c5d83f09c6e057ae1088ce87133ff156086bf02a2b8b2ec24e817"},
 {"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
 {"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
+{"name":"gitlab-cloud-connector","version":"0.2.1","platform":"ruby","checksum":"552d760ee2a9d25f681c9b2cf677e9f1b3c65f7516b6348e21b3bdf970640db4"},
 {"name":"gitlab-dangerfiles","version":"4.8.0","platform":"ruby","checksum":"b327d079552ec974a63bf34d749a0308425af6ebf51d01064f1a6ff216a523db"},
 {"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},
 {"name":"gitlab-fog-azure-rm","version":"2.2.0","platform":"ruby","checksum":"31aa7c2170f57874053144e7f716ec9e15f32e71ffbd2c56753dce46e2e78ba9"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 954dcdc8c00db2a39f1a65ac9332455d93d0a57d..b8ca460ec714ede060de1455ac84fa4f6bd0f945 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -729,6 +729,9 @@ GEM
       terminal-table (>= 1.5.1)
     gitlab-chronic (0.10.5)
       numerizer (~> 0.2)
+    gitlab-cloud-connector (0.2.1)
+      activesupport (~> 7.0)
+      jwt (~> 2.9.3)
     gitlab-dangerfiles (4.8.0)
       danger (>= 9.3.0)
       danger-gitlab (>= 8.0.0)
@@ -2067,6 +2070,7 @@ DEPENDENCIES
   gitaly (~> 17.5.0.pre.rc1)
   gitlab-backup-cli!
   gitlab-chronic (~> 0.10.5)
+  gitlab-cloud-connector (~> 0.2.1)
   gitlab-dangerfiles (~> 4.8.0)
   gitlab-duo-workflow-service-client (~> 0.1)!
   gitlab-experiment (~> 0.9.1)
diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum
index da5add729d22e002f30ea4556ac11a81a3d2b36f..fd241825a760cd123352f8a18bea8982a51d2226 100644
--- a/Gemfile.next.checksum
+++ b/Gemfile.next.checksum
@@ -221,6 +221,7 @@
 {"name":"gitaly","version":"17.5.0.pre.rc42","platform":"ruby","checksum":"15469230245c5d83f09c6e057ae1088ce87133ff156086bf02a2b8b2ec24e817"},
 {"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
 {"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
+{"name":"gitlab-cloud-connector","version":"0.2.1","platform":"ruby","checksum":"552d760ee2a9d25f681c9b2cf677e9f1b3c65f7516b6348e21b3bdf970640db4"},
 {"name":"gitlab-dangerfiles","version":"4.8.0","platform":"ruby","checksum":"b327d079552ec974a63bf34d749a0308425af6ebf51d01064f1a6ff216a523db"},
 {"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},
 {"name":"gitlab-fog-azure-rm","version":"2.2.0","platform":"ruby","checksum":"31aa7c2170f57874053144e7f716ec9e15f32e71ffbd2c56753dce46e2e78ba9"},
diff --git a/Gemfile.next.lock b/Gemfile.next.lock
index bd7ec9b03a11f450f9b4ef3a0f8f43da8b77e063..ea2dcfc0d34bf1a96792ee6df7ccf7ba490cc6f7 100644
--- a/Gemfile.next.lock
+++ b/Gemfile.next.lock
@@ -739,6 +739,9 @@ GEM
       terminal-table (>= 1.5.1)
     gitlab-chronic (0.10.5)
       numerizer (~> 0.2)
+    gitlab-cloud-connector (0.2.1)
+      activesupport (~> 7.0)
+      jwt (~> 2.9.3)
     gitlab-dangerfiles (4.8.0)
       danger (>= 9.3.0)
       danger-gitlab (>= 8.0.0)
@@ -2094,6 +2097,7 @@ DEPENDENCIES
   gitaly (~> 17.5.0.pre.rc1)
   gitlab-backup-cli!
   gitlab-chronic (~> 0.10.5)
+  gitlab-cloud-connector (~> 0.2.1)
   gitlab-dangerfiles (~> 4.8.0)
   gitlab-duo-workflow-service-client (~> 0.1)!
   gitlab-experiment (~> 0.9.1)
diff --git a/ee/lib/cloud_connector/self_signed/available_service_data.rb b/ee/lib/cloud_connector/self_signed/available_service_data.rb
index af8c5eb1199ddfc4d4f77eab177befb174b4f71b..2edb86474181a83b25492e98636b1264e52f455a 100644
--- a/ee/lib/cloud_connector/self_signed/available_service_data.rb
+++ b/ee/lib/cloud_connector/self_signed/available_service_data.rb
@@ -18,7 +18,7 @@ def initialize(name, cut_off_date, bundled_with, backend)
       override :access_token
       def access_token(resource = nil, extra_claims: {})
         if Feature.enabled?(:cloud_connector_jwt_replace, gitlab_org_group)
-          ::Gitlab::CloudConnector::JSONWebToken.new(
+          ::Gitlab::CloudConnector::JsonWebToken.new(
             issuer: Doorkeeper::OpenidConnect.configuration.issuer,
             audience: backend,
             subject: Gitlab::CurrentSettings.uuid,
diff --git a/ee/lib/gitlab/cloud_connector/json_web_token.rb b/ee/lib/gitlab/cloud_connector/json_web_token.rb
deleted file mode 100644
index 13401ef8cc55392120c763eebf92cc954ef8c7b2..0000000000000000000000000000000000000000
--- a/ee/lib/gitlab/cloud_connector/json_web_token.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
-  module CloudConnector
-    class JSONWebToken
-      SIGNING_ALGORITHM = 'RS256'
-      NOT_BEFORE_TIME = 5.seconds.to_i
-
-      attr_reader :issued_at, :expires_at
-
-      def initialize(issuer:, audience:, subject:, realm:, scopes:, ttl:, extra_claims: {})
-        @id = SecureRandom.uuid
-        @audience = audience
-        @subject = subject
-        @issuer = issuer
-        @issued_at = Time.current.to_i
-        @not_before = @issued_at - NOT_BEFORE_TIME
-        @expires_at = (@issued_at + ttl).to_i
-        @realm = realm
-        @scopes = scopes
-        @extra_claims = extra_claims
-      end
-
-      # jwk:
-      #   The key (pair) as an instance of JWT::JWK.
-      #
-      # Returns a signed and Base64-encoded JSON Web Token string, to be
-      # written to the HTTP Authorization header field.
-      def encode(jwk)
-        header_fields = { typ: 'JWT', kid: jwk.kid }
-
-        JWT.encode(payload, jwk.signing_key, SIGNING_ALGORITHM, header_fields)
-      end
-
-      def payload
-        {
-          jti: @id,
-          aud: @audience,
-          sub: @subject,
-          iss: @issuer,
-          iat: @issued_at,
-          nbf: @not_before,
-          exp: @expires_at
-        }.merge(cloud_connector_claims)
-      end
-
-      private
-
-      def cloud_connector_claims
-        {
-          gitlab_realm: @realm,
-          scopes: @scopes
-        }.merge(@extra_claims)
-      end
-    end
-  end
-end
diff --git a/ee/spec/lib/cloud_connector/self_signed/available_service_data_spec.rb b/ee/spec/lib/cloud_connector/self_signed/available_service_data_spec.rb
index 70c3ec8c0d0439a7e10add9f9fa727c24549cadb..168248dd02db13ba506d961513c301b2cb63abbe 100644
--- a/ee/spec/lib/cloud_connector/self_signed/available_service_data_spec.rb
+++ b/ee/spec/lib/cloud_connector/self_signed/available_service_data_spec.rb
@@ -34,7 +34,7 @@
 
     shared_examples 'issue a token with scopes' do
       let(:expected_token) do
-        instance_double('Gitlab::CloudConnector::JSONWebToken')
+        instance_double('Gitlab::CloudConnector::JsonWebToken')
       end
 
       before do
@@ -44,7 +44,7 @@
       end
 
       it 'returns the encoded token' do
-        expect(Gitlab::CloudConnector::JSONWebToken).to receive(:new).with(
+        expect(Gitlab::CloudConnector::JsonWebToken).to receive(:new).with(
           issuer: issuer,
           audience: backend,
           subject: instance_id,
diff --git a/ee/spec/lib/gitlab/cloud_connector/json_web_token_spec.rb b/ee/spec/lib/gitlab/cloud_connector/json_web_token_spec.rb
deleted file mode 100644
index a3ede2a75f5b66d75bf8b87db6320b06ff74c088..0000000000000000000000000000000000000000
--- a/ee/spec/lib/gitlab/cloud_connector/json_web_token_spec.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::CloudConnector::JSONWebToken, feature_category: :cloud_connector do
-  let(:extra_claims) { {} }
-
-  let(:expected_issuer) { 'gitlab.com' }
-  let(:expected_audience) { 'gitlab-ai-gateway' }
-  let(:expected_subject) { 'ABC-123' }
-  let(:expected_realm) { 'saas' }
-  let(:expected_scopes) { [:code_suggestions] }
-  let(:expected_ttl) { 10.minutes }
-
-  subject(:token) do
-    described_class.new(
-      issuer: expected_issuer,
-      audience: expected_audience,
-      subject: expected_subject,
-      realm: expected_realm,
-      scopes: expected_scopes,
-      ttl: expected_ttl,
-      extra_claims: extra_claims
-    )
-  end
-
-  describe '#payload' do
-    subject(:payload) { token.payload }
-
-    it 'has expected values', :freeze_time, :aggregate_failures do
-      now = Time.current.to_i
-
-      # standard claims
-      expect(payload[:iss]).to eq(expected_issuer)
-      expect(payload[:aud]).to eq(expected_audience)
-      expect(payload[:sub]).to eq(expected_subject)
-      expect(payload[:iat]).to eq(now)
-      expect(payload[:nbf]).to eq(now - 5.seconds)
-      expect(payload[:exp]).to eq(now + 10.minutes)
-
-      # cloud connector specific claims
-      expect(payload[:gitlab_realm]).to eq(expected_realm)
-      expect(payload[:scopes]).to eq(expected_scopes)
-    end
-
-    context 'when passing extra claims' do
-      let(:extra_claims) { { custom: 123 } }
-
-      it 'includes them in payload' do
-        expect(payload[:custom]).to eq(123)
-      end
-    end
-  end
-
-  describe '#encode' do
-    let(:rsa_key) { ::JWT::JWK.new(OpenSSL::PKey::RSA.new(2048)) }
-
-    subject(:encoded_token) { token.encode(rsa_key) }
-
-    it 'encodes token instance to string' do
-      expect(encoded_token).to be_instance_of(String)
-    end
-
-    it 'decodes successfully with public key', :aggregate_failures, :freeze_time do
-      now = Time.current.to_i
-      payload, header = JWT.decode(encoded_token, rsa_key.public_key, true, { algorithm: 'RS256' })
-
-      expect(header).to match(
-        "alg" => "RS256",
-        "typ" => "JWT",
-        "kid" => be_instance_of(String)
-      )
-      expect(payload).to match(
-        "jti" => be_instance_of(String),
-        "aud" => expected_audience,
-        "sub" => expected_subject,
-        "iss" => expected_issuer,
-        "iat" => now.to_i,
-        "nbf" => (now - 5.seconds).to_i,
-        "exp" => (now + 10.minutes).to_i,
-        "gitlab_realm" => expected_realm,
-        "scopes" => ["code_suggestions"]
-      )
-    end
-  end
-end
diff --git a/ee/spec/requests/api/internal/ai/x_ray/scan_spec.rb b/ee/spec/requests/api/internal/ai/x_ray/scan_spec.rb
index 52dd5601dcc81d1f9a345c3cce9e8f04a7cf649f..9165c6eed0f823e2c375ec2a291c2b81518b5ad3 100644
--- a/ee/spec/requests/api/internal/ai/x_ray/scan_spec.rb
+++ b/ee/spec/requests/api/internal/ai/x_ray/scan_spec.rb
@@ -194,7 +194,7 @@
       end
 
       before do
-        allow_next_instance_of(::Gitlab::CloudConnector::JSONWebToken) do |token|
+        allow_next_instance_of(::Gitlab::CloudConnector::JsonWebToken) do |token|
           allow(token).to receive(:encode).and_return(ai_gateway_token)
         end
       end
@@ -422,7 +422,7 @@ def request
       end
 
       before do
-        allow_next_instance_of(::Gitlab::CloudConnector::JSONWebToken) do |token|
+        allow_next_instance_of(::Gitlab::CloudConnector::JsonWebToken) do |token|
           allow(token).to receive(:encode).and_return(ai_gateway_token)
         end
       end