diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 71460a17a7540ca7bc08e0bb889e0fd2ad9cee5a..a6064239ead98e04c250571467dff73f93f8bb4f 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -77,6 +77,26 @@ def runner_setup_scripts
         private_runner_setup_scripts
       end
 
+      def export_job_token_authorizations
+        response = ::Ci::JobToken::ExportAuthorizationsService
+          .new(current_user: current_user, accessed_project: @project)
+          .execute
+
+        respond_to do |format|
+          format.csv do
+            if response.success?
+              send_data(response.payload.fetch(:data),
+                type: 'text/csv; charset=utf-8',
+                filename: response.payload.fetch(:filename))
+            else
+              flash[:alert] = _('Failed to generate export')
+
+              redirect_to project_settings_ci_cd_path(@project)
+            end
+          end
+        end
+      end
+
       private
 
       def authorize_reset_cache!
diff --git a/app/models/ci/job_token/authorization.rb b/app/models/ci/job_token/authorization.rb
index 7a6b48b823428dcffbe8326ab0119616499f0ffa..106f93da4286a8d88c63fa75c39b3435f525b87a 100644
--- a/app/models/ci/job_token/authorization.rb
+++ b/app/models/ci/job_token/authorization.rb
@@ -17,6 +17,9 @@ class Authorization < Ci::ApplicationRecord
       REQUEST_CACHE_KEY = :job_token_authorizations
       CAPTURE_DELAY = 5.minutes
 
+      scope :for_project, ->(accessed_project) { where(accessed_project: accessed_project) }
+      scope :preload_origin_project, -> { includes(origin_project: :route) }
+
       # Record in SafeRequestStore a cross-project access attempt
       def self.capture(origin_project:, accessed_project:)
         # Skip self-referential accesses as they are always allowed and don't need
diff --git a/app/services/ci/job_token/export_authorizations_service.rb b/app/services/ci/job_token/export_authorizations_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cb18548e5c0ba2c7ee13537a36cdada91c48ab78
--- /dev/null
+++ b/app/services/ci/job_token/export_authorizations_service.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# This class exports CI job token authorizations to a given project.
+module Ci
+  module JobToken
+    class ExportAuthorizationsService
+      include Gitlab::Allowable
+
+      def initialize(current_user:, accessed_project:)
+        @current_user = current_user
+        @accessed_project = accessed_project
+      end
+
+      def execute
+        unless can?(current_user, :admin_project, accessed_project)
+          return ServiceResponse.error(message: _('Access denied'), reason: :forbidden)
+        end
+
+        csv_data = CsvBuilder.new(authorizations, header_to_value_hash).render
+
+        ServiceResponse.success(payload: { data: csv_data, filename: csv_filename })
+      end
+
+      private
+
+      attr_reader :accessed_project, :current_user
+
+      def authorizations
+        Ci::JobToken::Authorization.for_project(accessed_project).preload_origin_project
+      end
+
+      def header_to_value_hash
+        {
+          'Origin Project Path' => ->(auth) { auth.origin_project.full_path },
+          'Last Authorized At (UTC)' => ->(auth) { auth.last_authorized_at.utc.iso8601 }
+        }
+      end
+
+      def csv_filename
+        "job-token-authorizations-#{accessed_project.id}-#{Time.current.to_fs(:number)}.csv"
+      end
+    end
+  end
+end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 1ece758fa261cd5fc549a2eaae97a885ec9bb2c3..c823079da71afb807611bd26be5176a24c96a8f9 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -118,6 +118,7 @@
             put :reset_registration_token
             post :create_deploy_token, path: 'deploy_token/create', to: 'repository#create_deploy_token'
             get :runner_setup_scripts, format: :json
+            get :export_job_token_authorizations, format: :csv
           end
 
           resource :operations, only: [:show, :update] do
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 56a2488692be6bcec665db7edfd0667204dc1e83..8d975bf58b5a1f081fc9141dd13f9993cc64ffd0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -22935,6 +22935,9 @@ msgstr ""
 msgid "Failed to generate description"
 msgstr ""
 
+msgid "Failed to generate export"
+msgstr ""
+
 msgid "Failed to generate export, please try again later."
 msgstr ""
 
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index 18e57e78724afc2eb3de4db51943d5a80f28856b..d5b5f137267e932880cee06c914929e5c357b61a 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -401,6 +401,49 @@ def show
         expect(json_response).to have_key("errors")
       end
     end
+
+    describe 'GET #export_job_token_authorizations' do
+      subject(:get_authorizations) do
+        get :export_job_token_authorizations, params: {
+          namespace_id: project.namespace,
+          project_id: project
+        }, format: :csv
+      end
+
+      let!(:authorizations) do
+        create_list(:ci_job_token_authorization, 3, accessed_project: project)
+      end
+
+      context 'when the export is successful' do
+        it 'renders the CSV' do
+          get_authorizations
+
+          expect(response).to have_gitlab_http_status(:ok)
+          rows = response.body.lines
+
+          expect(rows[0]).to include('Origin Project Path,Last Authorized At (UTC)')
+          expect(rows[1]).to include(authorizations.first.origin_project.full_path)
+          expect(rows[1]).to include(authorizations.first.last_authorized_at.utc.iso8601)
+        end
+      end
+
+      context 'when the export fails' do
+        let(:export_service) { instance_double(Ci::JobToken::ExportAuthorizationsService) }
+        let(:failed_response) { ServiceResponse.error(message: 'Export failed') }
+
+        before do
+          allow(::Ci::JobToken::ExportAuthorizationsService).to receive(:new).and_return(export_service)
+          allow(export_service).to receive(:execute).and_return(failed_response)
+        end
+
+        it 'sets a flash alert and redirects to the project CI/CD settings' do
+          get_authorizations
+
+          expect(flash[:alert]).to eq('Failed to generate export')
+          expect(response).to redirect_to(project_settings_ci_cd_path(project))
+        end
+      end
+    end
   end
 
   context 'as a developer' do
diff --git a/spec/models/ci/job_token/authorization_spec.rb b/spec/models/ci/job_token/authorization_spec.rb
index 933010bfbab3e666f9239fdc8bd43fce05db7946..be3bc2f34abbda0f436d0786f91543d90041cab7 100644
--- a/spec/models/ci/job_token/authorization_spec.rb
+++ b/spec/models/ci/job_token/authorization_spec.rb
@@ -107,4 +107,39 @@
       end
     end
   end
+
+  describe '.preload_origin_project' do
+    before do
+      create_list(:ci_job_token_authorization, 5)
+    end
+
+    it 'does not perform N+1 queries' do
+      control = ActiveRecord::QueryRecorder.new do
+        described_class.preload_origin_project.map { |a| a.origin_project.full_path }
+      end
+
+      create(:ci_job_token_authorization)
+
+      expect do
+        described_class.preload_origin_project.map { |a| a.origin_project.full_path }
+      end.not_to exceed_query_limit(control)
+    end
+  end
+
+  describe '.for_project scope' do
+    let(:project) { create(:project) }
+
+    let!(:current_authorizations) do
+      create_list(:ci_job_token_authorization, 2, accessed_project: project)
+    end
+
+    let!(:other_authorization) { create(:ci_job_token_authorization) }
+
+    it 'contains only the authorizations targeting the project' do
+      authorizations = described_class.for_project(project)
+      expect(authorizations).to eq(current_authorizations)
+
+      expect(authorizations).not_to include(other_authorization)
+    end
+  end
 end
diff --git a/spec/services/ci/job_token/export_authorizations_service_spec.rb b/spec/services/ci/job_token/export_authorizations_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..936f5a54eab1fb68b971f74b6b146cefed627e58
--- /dev/null
+++ b/spec/services/ci/job_token/export_authorizations_service_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobToken::ExportAuthorizationsService, feature_category: :secrets_management do
+  let_it_be_with_refind(:user) { create(:user) }
+  let_it_be_with_refind(:project) { create(:project) }
+  let_it_be(:origin_project) { create(:project) }
+
+  let(:accessed_project) { project }
+  let(:service) { described_class.new(current_user: user, accessed_project: accessed_project) }
+
+  before_all do
+    project.add_maintainer(user)
+  end
+
+  describe '#execute' do
+    context 'when user has admin access to the project' do
+      let!(:authorizations) do
+        create_list(:ci_job_token_authorization, 2, accessed_project: accessed_project)
+      end
+
+      it 'returns a success response with CSV data' do
+        result = service.execute
+
+        expect(result).to be_success
+
+        expect(result.payload[:filename]).to start_with("job-token-authorizations-#{accessed_project.id}-")
+
+        rows = result.payload[:data].lines
+
+        expect(rows.count).to eq(3) # 2 authorizations + 1 header row
+
+        expect(rows[0]).to include('Origin Project Path,Last Authorized At (UTC)')
+        expect(rows[1]).to include(authorizations.first.origin_project.full_path)
+        expect(rows[1]).to include(authorizations.first.last_authorized_at.utc.iso8601)
+      end
+    end
+
+    context 'when user does not have admin access to the project' do
+      let(:accessed_project) { create(:project) }
+
+      it 'returns an error response' do
+        result = service.execute
+
+        expect(result).to be_error
+        expect(result.message).to eq('Access denied')
+        expect(result.reason).to eq(:forbidden)
+      end
+    end
+  end
+end