diff --git a/ee/lib/gitlab_subscriptions/api/internal/api.rb b/ee/lib/gitlab_subscriptions/api/internal/api.rb
index bdd70aa715ed08f6717c3fc59d008d5553806270..4b451cec58018ebd0f9bf56132b076dea6911ee4 100644
--- a/ee/lib/gitlab_subscriptions/api/internal/api.rb
+++ b/ee/lib/gitlab_subscriptions/api/internal/api.rb
@@ -4,8 +4,14 @@ module GitlabSubscriptions
   module API
     module Internal
       class API < ::API::Base
+        helpers GitlabSubscriptions::API::Internal::Helpers
+
         before do
-          authenticated_as_admin!
+          if jwt_request?
+            authenticate_from_jwt!
+          else
+            authenticated_as_admin!
+          end
         end
 
         mount ::GitlabSubscriptions::API::Internal::Members
diff --git a/ee/lib/gitlab_subscriptions/api/internal/auth.rb b/ee/lib/gitlab_subscriptions/api/internal/auth.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f22e2a458b21e051e78a1601f78d1993f094aeb1
--- /dev/null
+++ b/ee/lib/gitlab_subscriptions/api/internal/auth.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module GitlabSubscriptions
+  module API
+    module Internal
+      class Auth
+        include Gitlab::Utils::StrongMemoize
+
+        INTERNAL_API_REQUEST_HEADER = 'X-Customers-Dot-Internal-Token'
+        AUDIENCE = 'gitlab-subscriptions'
+        SUBJECT = 'customers-dot-internal-api'
+
+        def self.verify_api_request(headers)
+          token = headers[INTERNAL_API_REQUEST_HEADER]
+
+          new(token: token).decode if token.present?
+        end
+
+        def initialize(token:)
+          @token = token
+        end
+
+        def decode
+          return unless openid_configuration.present?
+          return unless jwks.present?
+
+          JWT.decode(token, nil, true, options)
+        rescue JWT::DecodeError, JWT::ExpiredSignature
+          nil
+        end
+
+        private
+
+        attr_reader :token
+
+        def options
+          {
+            algorithms: openid_configuration['id_token_signing_alg_values_supported'],
+            jwks: jwks,
+            iss: openid_configuration['issuer'],
+            verify_iss: true,
+            sub: SUBJECT,
+            verify_sub: true,
+            aud: AUDIENCE,
+            verify_aud: true
+          }
+        end
+
+        def jwks
+          response = Gitlab::HTTP.get(openid_configuration['jwks_uri'])
+
+          return unless response.ok?
+
+          response.parsed_response
+        end
+        strong_memoize_attr :jwks
+
+        def openid_configuration
+          response = Gitlab::HTTP.get(
+            "#{Gitlab::Routing.url_helpers.subscription_portal_url}/.well-known/openid-configuration"
+          )
+
+          return {} unless response.ok?
+
+          response.parsed_response
+        end
+        strong_memoize_attr :openid_configuration
+      end
+    end
+  end
+end
diff --git a/ee/lib/gitlab_subscriptions/api/internal/helpers.rb b/ee/lib/gitlab_subscriptions/api/internal/helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9885adf67fcecc15e9025e8809f8a5af32b71369
--- /dev/null
+++ b/ee/lib/gitlab_subscriptions/api/internal/helpers.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module GitlabSubscriptions
+  module API
+    module Internal
+      module Helpers
+        def jwt_request?
+          headers[Auth::INTERNAL_API_REQUEST_HEADER].present?
+        end
+
+        def authenticate_from_jwt!
+          unauthorized! unless Auth.verify_api_request(headers)
+        end
+      end
+    end
+  end
+end
diff --git a/ee/lib/gitlab_subscriptions/api/internal/namespaces.rb b/ee/lib/gitlab_subscriptions/api/internal/namespaces.rb
index 283d7059264babca462d5f3b1b7bd1ae092ada64..457ff6206a277c8db790a3f3e9a7897bbb0ce304 100644
--- a/ee/lib/gitlab_subscriptions/api/internal/namespaces.rb
+++ b/ee/lib/gitlab_subscriptions/api/internal/namespaces.rb
@@ -4,7 +4,7 @@ module GitlabSubscriptions
   module API
     module Internal
       class Namespaces < ::API::Base
-        feature_category :subscription_management
+        feature_category :plan_provisioning
         urgency :low
 
         namespace :internal do
diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/auth_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/auth_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d27347ccc67305aa15f19e977dc81835dafa4a27
--- /dev/null
+++ b/ee/spec/requests/gitlab_subscriptions/api/internal/auth_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSubscriptions::API::Internal::Auth, :aggregate_failures, :api, feature_category: :plan_provisioning do
+  describe '.verify_api_request' do
+    let_it_be(:internal_api_jwk) { ::JWT::JWK.new(OpenSSL::PKey.generate_key('RSA')) }
+    let_it_be(:unrelated_jwk) { ::JWT::JWK.new(OpenSSL::PKey.generate_key('RSA')) }
+
+    context 'when the request does not have the internal token header' do
+      it 'returns nil' do
+        headers = { 'Other-Header' => 'test-token' }
+
+        expect(described_class.verify_api_request(headers)).to be_nil
+      end
+    end
+
+    context 'when the open ID configuration cannot be fetched' do
+      it 'returns nil' do
+        stub_open_id_configuration(success: false, json: {})
+
+        token = generate_token(jwk: internal_api_jwk, payload: jwt_payload)
+
+        expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil
+      end
+    end
+
+    context 'when the JWKS cannot be fetched' do
+      it 'returns nil' do
+        stub_open_id_configuration
+        stub_keys_discovery(success: false)
+
+        token = generate_token(jwk: internal_api_jwk, payload: jwt_payload)
+
+        expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil
+      end
+    end
+
+    context 'when the JWKs can be fetched from the subscription portal', :freeze_time do
+      before do
+        stub_open_id_configuration
+        stub_keys_discovery(jwks: [unrelated_jwk, internal_api_jwk])
+      end
+
+      context 'when the token has the wrong issuer' do
+        it 'returns nil' do
+          token = generate_token(
+            jwk: internal_api_jwk,
+            payload: jwt_payload(iss: 'some-other-issuer')
+          )
+
+          expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil
+        end
+      end
+
+      context 'when the token has the wrong subject' do
+        it 'returns nil' do
+          token = generate_token(
+            jwk: internal_api_jwk,
+            payload: jwt_payload(sub: 'some-other-subject')
+          )
+
+          expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil
+        end
+      end
+
+      context 'when the token has the wrong audience' do
+        it 'returns nil' do
+          token = generate_token(
+            jwk: internal_api_jwk,
+            payload: jwt_payload(aud: 'some-other-audience')
+          )
+
+          expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil
+        end
+      end
+
+      context 'when the token has expired' do
+        it 'returns nil' do
+          token = generate_token(
+            jwk: internal_api_jwk,
+            payload: jwt_payload(iat: 10.minutes.ago.to_i, exp: 5.minutes.ago.to_i)
+          )
+
+          expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil
+        end
+      end
+
+      context 'when the token cannot be decoded using the CustomersDot JWKs' do
+        it 'returns nil' do
+          token = generate_token(
+            jwk: ::JWT::JWK.new(OpenSSL::PKey.generate_key('RSA')),
+            payload: jwt_payload
+          )
+
+          expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to be_nil
+        end
+      end
+
+      context 'when the token can be decoded using CustomersDot JWKs' do
+        it 'returns the decoded JWT' do
+          token = generate_token(jwk: internal_api_jwk, payload: jwt_payload)
+
+          expect(described_class.verify_api_request({ 'X-Customers-Dot-Internal-Token' => token })).to match_array(
+            [jwt_payload.stringify_keys, { 'typ' => 'JWT', 'kid' => internal_api_jwk.kid, 'alg' => 'RS256' }]
+          )
+        end
+      end
+    end
+
+    def generate_token(jwk:, payload:)
+      JWT.encode(payload, jwk.keypair, 'RS256', { typ: 'JWT', kid: jwk.kid })
+    end
+
+    def jwt_payload(**options)
+      {
+        aud: 'gitlab-subscriptions',
+        sub: 'customers-dot-internal-api',
+        iss: "#{Gitlab::Routing.url_helpers.subscription_portal_url}/",
+        exp: (Time.current.to_i + 5.minutes.to_i)
+      }.merge(options)
+    end
+
+    def stub_open_id_configuration(success: true, json: nil)
+      subscriptions_host = Gitlab::Routing.url_helpers.subscription_portal_url
+      response_json = json || {
+        'issuer' => "#{subscriptions_host}/",
+        'jwks_uri' => "#{subscriptions_host}/oauth/discovery/keys",
+        'id_token_signing_alg_values_supported' => ['RS256']
+      }
+
+      gitlab_http_response = instance_double(HTTParty::Response, ok?: success, parsed_response: response_json)
+
+      allow(Gitlab::HTTP)
+        .to receive(:get)
+        .with("#{subscriptions_host}/.well-known/openid-configuration")
+        .and_return(gitlab_http_response)
+    end
+
+    def stub_keys_discovery(success: true, jwks: [])
+      response_json = {
+        'keys' => jwks.map { |jwk| jwk.export.merge('use' => 'sig', 'alg' => 'RS256') }
+      }
+
+      gitlab_http_response = instance_double(HTTParty::Response, ok?: success, parsed_response: response_json)
+
+      allow(Gitlab::HTTP)
+        .to receive(:get)
+        .with("#{Gitlab::Routing.url_helpers.subscription_portal_url}/oauth/discovery/keys")
+        .and_return(gitlab_http_response)
+    end
+  end
+end
diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/helpers_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/helpers_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cea8ec650202b26a57dba3ff3dbaddf7ac1958b3
--- /dev/null
+++ b/ee/spec/requests/gitlab_subscriptions/api/internal/helpers_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSubscriptions::API::Internal::Helpers, feature_category: :plan_provisioning do
+  using RSpec::Parameterized::TableSyntax
+
+  subject(:helper) { Class.new.include(described_class).new }
+
+  describe '#jwt_request?' do
+    context 'when the headers do not contain the subscription portal JWT token' do
+      it 'returns false' do
+        allow(helper).to receive(:headers).and_return({ 'Authorization' => 'Bearer test' })
+
+        expect(helper.jwt_request?).to eq(false)
+      end
+    end
+
+    context 'when the headers contain the subscription portal JWT token' do
+      it 'returns true' do
+        allow(helper).to receive(:headers).and_return({ 'X-Customers-Dot-Internal-Token' => 'test-token' })
+
+        expect(helper.jwt_request?).to eq(true)
+      end
+    end
+  end
+
+  describe '#authenticate_from_jwt!' do
+    let(:jwt_token_headers) { { 'X-Customers-Dot-Internal-Token' => 'test-token' } }
+
+    before do
+      allow(helper).to receive(:headers).and_return(jwt_token_headers)
+    end
+
+    context 'when the request cannot be verified with the subscription portal JWT token' do
+      it 'returns an unauthorised error' do
+        allow(helper).to receive(:unauthorized!).and_raise('unauthorized')
+
+        allow(GitlabSubscriptions::API::Internal::Auth)
+          .to receive(:verify_api_request)
+          .with(jwt_token_headers)
+          .and_return(nil)
+
+        expect { helper.authenticate_from_jwt! }.to raise_error('unauthorized')
+      end
+    end
+
+    context 'when the request can be verified with the subscription portal JWT token' do
+      it 'does not return an error' do
+        allow(GitlabSubscriptions::API::Internal::Auth)
+          .to receive(:verify_api_request)
+          .with(jwt_token_headers)
+          .and_return(['decoded-token'])
+
+        expect(helper.authenticate_from_jwt!).to eq(nil)
+      end
+    end
+  end
+end
diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/members_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/members_spec.rb
index 28bc4673f9fe0a4514a5231d6ab50cfed54160c6..b963ccc1d6e898c6dd7fab70650fe8d23ee6437b 100644
--- a/ee/spec/requests/gitlab_subscriptions/api/internal/members_spec.rb
+++ b/ee/spec/requests/gitlab_subscriptions/api/internal/members_spec.rb
@@ -4,38 +4,30 @@
 
 RSpec.describe GitlabSubscriptions::API::Internal::Members, :aggregate_failures, :api, feature_category: :subscription_management do
   describe 'GET /internal/gitlab_subscriptions/namespaces/:id/owners', :saas do
-    let_it_be(:namespace) { create(:group) }
-    let(:namespace_id) { namespace.id }
-    let(:owners_path) { "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/owners" }
+    include GitlabSubscriptions::InternalApiHelpers
 
-    context 'when unauthenticated' do
-      it 'returns authentication error' do
-        get api(owners_path)
+    let_it_be(:namespace) { create(:group) }
 
-        expect(response).to have_gitlab_http_status(:unauthorized)
-      end
+    def owners_path(namespace_id)
+      internal_api("namespaces/#{namespace_id}/owners")
     end
 
-    context 'when authenticated as user' do
+    context 'when unauthenticated' do
       it 'returns authentication error' do
-        get api(owners_path, create(:user))
+        get owners_path(namespace.id)
 
-        expect(response).to have_gitlab_http_status(:forbidden)
+        expect(response).to have_gitlab_http_status(:unauthorized)
       end
     end
 
-    context 'when authenticated as admin' do
-      let_it_be(:admin) { create(:admin) }
-
-      subject(:get_owners) do
-        get api(owners_path, admin, admin_mode: true)
+    context 'when authenticated as the subscription portal' do
+      before do
+        stub_internal_api_authentication
       end
 
       context 'when the namespace cannot be found' do
-        let(:namespace_id) { -1 }
-
         it 'returns an error response' do
-          get_owners
+          get owners_path(non_existing_record_id), headers: internal_api_headers
 
           expect(response).to have_gitlab_http_status(:not_found)
           expect(json_response['message']).to eq('404 Group Namespace Not Found')
@@ -44,7 +36,7 @@
 
       context 'when the namespace does not have any owners' do
         it 'returns an empty response' do
-          get_owners
+          get owners_path(namespace.id), headers: internal_api_headers
 
           expect(response).to have_gitlab_http_status(:ok)
           expect(json_response).to be_empty
@@ -93,7 +85,7 @@
             'X-Total-Pages' => '1'
           }
 
-          get_owners
+          get owners_path(namespace.id), headers: internal_api_headers
 
           expect(response).to have_gitlab_http_status(:ok)
           expect(response.headers).to match(hash_including(expected_pagination_headers))
@@ -108,7 +100,7 @@
           end
 
           it 'does not return inactive users' do
-            get_owners
+            get owners_path(namespace.id), headers: internal_api_headers
 
             expect(response).to have_gitlab_http_status(:ok)
             expect(json_response.count).to eq(1)
@@ -117,5 +109,109 @@
         end
       end
     end
+
+    # this method of authentication is deprecated and will be removed in
+    # https://gitlab.com/gitlab-org/gitlab/-/issues/473625
+    context 'when authenticating with a personal access token' do
+      def owners_path(namespace_id)
+        "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/owners"
+      end
+
+      context 'when authenticated as user' do
+        it 'returns authentication error' do
+          get api(owners_path(namespace.id), create(:user))
+
+          expect(response).to have_gitlab_http_status(:forbidden)
+        end
+      end
+
+      context 'when authenticated as admin' do
+        let_it_be(:admin) { create(:admin) }
+
+        context 'when the namespace cannot be found' do
+          it 'returns an error response' do
+            get api(owners_path(non_existing_record_id), admin, admin_mode: true)
+
+            expect(response).to have_gitlab_http_status(:not_found)
+            expect(json_response['message']).to eq('404 Group Namespace Not Found')
+          end
+        end
+
+        context 'when the namespace does not have any owners' do
+          it 'returns an empty response' do
+            get api(owners_path(namespace.id), admin, admin_mode: true)
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(json_response).to be_empty
+          end
+        end
+
+        context 'when the namespace has owners and other members' do
+          let_it_be(:owner_1) { create(:user) }
+          let_it_be(:owner_2) { create(:user) }
+          let_it_be(:maintainer) { create(:user) }
+          let_it_be(:guest) { create(:user) }
+
+          let_it_be(:sub_group_owner) { create(:user) }
+          let_it_be(:sub_group) { create(:group, parent: namespace) }
+
+          before_all do
+            namespace.add_owner(owner_1)
+            namespace.add_owner(owner_2)
+
+            namespace.add_maintainer(maintainer)
+            namespace.add_guest(guest)
+
+            sub_group.add_owner(sub_group_owner)
+          end
+
+          it 'returns only direct owners of the namespace' do
+            expected_response = [
+              {
+                'user' => { 'id' => owner_1.id, 'username' => a_kind_of(String), 'name' => a_kind_of(String) },
+                'access_level' => 50,
+                'notification_email' => a_kind_of(String)
+              },
+              {
+                'user' => { 'id' => owner_2.id, 'username' => a_kind_of(String), 'name' => a_kind_of(String) },
+                'access_level' => 50,
+                'notification_email' => a_kind_of(String)
+              }
+            ]
+
+            expected_pagination_headers = {
+              'X-Per-Page' => '20',
+              'X-Page' => '1',
+              'X-Next-Page' => '',
+              'X-Prev-Page' => '',
+              'X-Total' => '2',
+              'X-Total-Pages' => '1'
+            }
+
+            get api(owners_path(namespace.id), admin, admin_mode: true)
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(response.headers).to match(hash_including(expected_pagination_headers))
+
+            expect(json_response.count).to eq(2)
+            expect(json_response).to match_array(expected_response)
+          end
+
+          context 'when the owner is inactive' do
+            before do
+              owner_2.block!
+            end
+
+            it 'does not return inactive users' do
+              get api(owners_path(namespace.id), admin, admin_mode: true)
+
+              expect(response).to have_gitlab_http_status(:ok)
+              expect(json_response.count).to eq(1)
+              expect(json_response.first['user']['id']).to eq(owner_1.id)
+            end
+          end
+        end
+      end
+    end
   end
 end
diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/namespaces_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/namespaces_spec.rb
index 5559c3f414f19e73d0007f58f8e477ba8705f912..44e01ebf9cba0eb448706bc5f1257c6f87cc9061 100644
--- a/ee/spec/requests/gitlab_subscriptions/api/internal/namespaces_spec.rb
+++ b/ee/spec/requests/gitlab_subscriptions/api/internal/namespaces_spec.rb
@@ -2,369 +2,618 @@
 
 require 'spec_helper'
 
-RSpec.describe GitlabSubscriptions::API::Internal::Namespaces, :aggregate_failures, :api, feature_category: :subscription_management do
+RSpec.describe GitlabSubscriptions::API::Internal::Namespaces, :saas, :aggregate_failures, :api, feature_category: :plan_provisioning do
   include AfterNextHelpers
+  include GitlabSubscriptions::InternalApiHelpers
 
   def namespace_path(namespace_id)
-    "/internal/gitlab_subscriptions/namespaces/#{namespace_id}"
+    internal_api("namespaces/#{namespace_id}")
   end
 
   describe 'GET /internal/gitlab_subscriptions/namespaces/:id' do
-    let_it_be(:admin) { create(:admin) }
     let_it_be(:namespace) { create(:group) }
 
     context 'when unauthenticated' do
       it 'returns an error response' do
-        get api(namespace_path(namespace.id))
+        get namespace_path(namespace.id)
 
         expect(response).to have_gitlab_http_status(:unauthorized)
       end
     end
 
-    context 'when the user is not an admin' do
-      it 'returns an error response' do
-        user = create(:user)
+    context 'when authenticated as the subscription portal' do
+      before do
+        stub_internal_api_authentication
+      end
 
-        get api(namespace_path(namespace.id), user)
+      context 'when the namespace cannot be found' do
+        it 'returns an error response' do
+          get namespace_path(non_existing_record_id), headers: internal_api_headers
 
-        expect(response).to have_gitlab_http_status(:forbidden)
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
       end
-    end
 
-    context 'when the admin is not in admin mode' do
-      it 'returns an error response' do
-        get api(namespace_path(namespace.id), admin, admin_mode: false)
+      context 'when fetching a group namespace' do
+        it 'successfully returns the namespace attributes' do
+          get namespace_path(namespace.id), headers: internal_api_headers
 
-        expect(response).to have_gitlab_http_status(:forbidden)
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response).to eq({
+            'id' => namespace.id,
+            'kind' => 'group',
+            'name' => namespace.name,
+            'parent_id' => nil,
+            'path' => namespace.path,
+            'full_path' => namespace.full_path,
+            'avatar_url' => nil,
+            'plan' => 'free',
+            'projects_count' => 0,
+            'root_repository_size' => nil,
+            'shared_runners_minutes_limit' => nil,
+            'trial' => false,
+            'trial_ends_on' => nil,
+            'web_url' => namespace.web_url,
+            'additional_purchased_storage_size' => 0,
+            'additional_purchased_storage_ends_on' => nil,
+            'billable_members_count' => 0,
+            'extra_shared_runners_minutes_limit' => nil,
+            'members_count_with_descendants' => 0
+          })
+        end
       end
-    end
 
-    context 'when the namespace cannot be found' do
-      it 'returns an error response' do
-        get api(namespace_path('0'), admin, admin_mode: true)
+      context 'when fetching a user namespace' do
+        it 'successfully returns the namespace attributes' do
+          user_namespace = create(:user, :with_namespace).namespace
 
-        expect(response).to have_gitlab_http_status(:not_found)
+          get namespace_path(user_namespace.id), headers: internal_api_headers
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response).to match(
+            'id' => user_namespace.id,
+            'kind' => 'user',
+            'name' => user_namespace.name,
+            'parent_id' => nil,
+            'path' => user_namespace.path,
+            'full_path' => user_namespace.full_path,
+            'avatar_url' => user_namespace.avatar_url,
+            'plan' => 'free',
+            'shared_runners_minutes_limit' => nil,
+            'trial' => false,
+            'trial_ends_on' => nil,
+            'web_url' => a_string_including(user_namespace.path),
+            'additional_purchased_storage_size' => 0,
+            'additional_purchased_storage_ends_on' => nil,
+            'billable_members_count' => 1,
+            'extra_shared_runners_minutes_limit' => nil
+          )
+        end
       end
     end
 
-    context 'when the namespace is of a group' do
-      it 'returns OK status and contains some set of keys' do
-        get api(namespace_path(namespace.id), admin, admin_mode: true)
-
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
-          'parent_id', 'members_count_with_descendants',
-          'plan', 'shared_runners_minutes_limit',
-          'avatar_url', 'web_url', 'trial_ends_on', 'trial',
-          'extra_shared_runners_minutes_limit', 'billable_members_count',
-          'root_repository_size', 'projects_count',
-          'additional_purchased_storage_size', 'additional_purchased_storage_ends_on')
+    # this method of authentication is deprecated and will be removed in
+    # https://gitlab.com/gitlab-org/gitlab/-/issues/473625
+    context 'when authenticating with an admin personal access token' do
+      let_it_be(:admin) { create(:admin) }
+
+      def namespace_path(namespace_id)
+        "/internal/gitlab_subscriptions/namespaces/#{namespace_id}"
       end
-    end
 
-    context 'when the namespace is of a user' do
-      let_it_be(:user) { create(:user, :with_namespace) }
-      let_it_be(:namespace) { user.namespace }
+      context 'when the user is not an admin' do
+        it 'returns an error response' do
+          user = create(:user)
 
-      it 'returns OK status and contains some set of keys' do
-        get api(namespace_path(namespace.id), admin, admin_mode: true)
+          get api(namespace_path(namespace.id), user)
 
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
-          'parent_id', 'plan', 'shared_runners_minutes_limit',
-          'avatar_url', 'web_url', 'trial_ends_on', 'trial',
-          'extra_shared_runners_minutes_limit', 'billable_members_count',
-          'additional_purchased_storage_size', 'additional_purchased_storage_ends_on')
+          expect(response).to have_gitlab_http_status(:forbidden)
+        end
       end
-    end
-  end
 
-  describe 'PUT /internal/gitlab_subscriptions/namespaces/:id' do
-    let(:admin) { create(:admin) }
-    let(:user) { create(:user) }
+      context 'when the admin is not in admin mode' do
+        it 'returns an error response' do
+          get api(namespace_path(namespace.id), admin, admin_mode: false)
 
-    let(:group1) { create(:group, :with_ci_minutes, ci_minutes_used: 1600) }
-    let_it_be(:group2) { create(:group, :nested) }
-    let_it_be(:ultimate_plan) { create(:ultimate_plan) }
-    let_it_be(:project) { create(:project, namespace: group2, name: group2.name, path: group2.path) }
-    let_it_be(:project_namespace) { project.project_namespace }
+          expect(response).to have_gitlab_http_status(:forbidden)
+        end
+      end
 
-    let(:usage) do
-      ::Ci::Minutes::NamespaceMonthlyUsage.current_month.find_by(namespace_id: group1)
-    end
+      context 'when the namespace cannot be found' do
+        it 'returns an error response' do
+          get api(namespace_path(non_existing_record_id), admin, admin_mode: true)
 
-    let(:params) do
-      {
-        shared_runners_minutes_limit: 9001,
-        additional_purchased_storage_size: 10_000,
-        additional_purchased_storage_ends_on: Date.today.to_s
-      }
-    end
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
+      end
+
+      context 'when the namespace is of a group' do
+        it 'returns OK status and contains some set of keys' do
+          get api(namespace_path(namespace.id), admin, admin_mode: true)
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
+            'parent_id', 'members_count_with_descendants',
+            'plan', 'shared_runners_minutes_limit',
+            'avatar_url', 'web_url', 'trial_ends_on', 'trial',
+            'extra_shared_runners_minutes_limit', 'billable_members_count',
+            'root_repository_size', 'projects_count',
+            'additional_purchased_storage_size', 'additional_purchased_storage_ends_on')
+        end
+      end
 
-    before do
-      usage.update!(notification_level: 30)
-      group1.update!(shared_runners_minutes_limit: 1000, extra_shared_runners_minutes_limit: 500)
+      context 'when the namespace is of a user' do
+        it 'returns OK status and contains some set of keys' do
+          user = create(:user, :with_namespace)
+
+          get api(namespace_path(user.namespace.id), admin, admin_mode: true)
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
+            'parent_id', 'plan', 'shared_runners_minutes_limit',
+            'avatar_url', 'web_url', 'trial_ends_on', 'trial',
+            'extra_shared_runners_minutes_limit', 'billable_members_count',
+            'additional_purchased_storage_size', 'additional_purchased_storage_ends_on')
+        end
+      end
     end
+  end
 
+  describe 'PUT /internal/gitlab_subscriptions/namespaces/:id' do
     context 'when unauthenticated' do
       it 'returns an error response' do
-        put api(namespace_path(group1.id))
+        put namespace_path(non_existing_record_id)
 
         expect(response).to have_gitlab_http_status(:unauthorized)
       end
     end
 
-    context 'when the user is not an admin' do
-      it 'returns an error response' do
-        user = create(:user)
+    context 'when authenticated as the subscription portal' do
+      before do
+        stub_internal_api_authentication
+      end
 
-        put api(namespace_path(group1.id), user)
+      context 'when the namespace cannot be found' do
+        it 'returns an error response' do
+          put namespace_path(non_existing_record_id), headers: internal_api_headers
 
-        expect(response).to have_gitlab_http_status(:forbidden)
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
       end
-    end
 
-    context 'when the admin is not in admin mode' do
-      it 'returns an error response' do
-        put api(namespace_path(group1.id), admin, admin_mode: false)
+      context 'when a project namespace ID is passed' do
+        it 'returns 404' do
+          project = create(:project)
 
-        expect(response).to have_gitlab_http_status(:forbidden)
+          put namespace_path(project.project_namespace.id), headers: internal_api_headers
+
+          expect(response).to have_gitlab_http_status(:not_found)
+          expect(json_response).to eq('message' => '404 Namespace Not Found')
+        end
       end
-    end
 
-    context 'when the namespace cannot be found' do
-      it 'returns an error response' do
-        put api(namespace_path('0'), admin, admin_mode: true)
+      context 'when updating gitlab subscription data' do
+        let_it_be(:root_namespace) { create(:namespace_with_plan) }
+
+        it "updates the gitlab_subscription record" do
+          existing_subscription = root_namespace.gitlab_subscription
+
+          params = {
+            gitlab_subscription_attributes: {
+              start_date: '2019-06-01',
+              end_date: '2020-06-01',
+              plan_code: 'ultimate',
+              seats: 20,
+              max_seats_used: 10,
+              auto_renew: true,
+              trial: true,
+              trial_ends_on: '2019-05-01',
+              trial_starts_on: '2019-06-01',
+              trial_extension_type: GitlabSubscription.trial_extension_types[:reactivated]
+            }
+          }
 
-        expect(response).to have_gitlab_http_status(:not_found)
+          put namespace_path(root_namespace.id), headers: internal_api_headers, params: params
+
+          expect(root_namespace.reload.gitlab_subscription.reload.seats).to eq 20
+          expect(root_namespace.gitlab_subscription).to eq existing_subscription
+        end
+
+        it 'returns a 400 error with invalid data' do
+          params = {
+            gitlab_subscription_attributes: {
+              start_date: nil,
+              end_date: '2020-06-01',
+              plan_code: 'ultimate',
+              seats: nil,
+              max_seats_used: 10,
+              auto_renew: true
+            }
+          }
+
+          put namespace_path(root_namespace.id), headers: internal_api_headers, params: params
+
+          expect(response).to have_gitlab_http_status(:bad_request)
+          expect(json_response['message']).to eq(
+            "gitlab_subscription.seats" => ["can't be blank"],
+            "gitlab_subscription.start_date" => ["can't be blank"]
+          )
+        end
       end
-    end
 
-    context 'when authenticated as admin' do
-      subject(:request) { put api(namespace_path(group1.id), admin, admin_mode: true), params: params }
+      describe 'runners minutes limits' do
+        let_it_be(:root_namespace) do
+          create(
+            :group,
+            :with_ci_minutes,
+            ci_minutes_used: 1600,
+            shared_runners_minutes_limit: 1000,
+            extra_shared_runners_minutes_limit: 500
+          )
+        end
+
+        context 'when updating the extra_shared_runners_minutes_limit' do
+          let(:params) { { extra_shared_runners_minutes_limit: 1000 } }
+
+          it 'updates the extra shared runners minutes limit' do
+            put namespace_path(root_namespace.id), headers: internal_api_headers, params: params
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(json_response['extra_shared_runners_minutes_limit'])
+              .to eq(params[:extra_shared_runners_minutes_limit])
+          end
+
+          it 'expires the compute minutes CachedQuota' do
+            expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!)
+
+            put namespace_path(root_namespace.id), headers: internal_api_headers, params: params
+          end
+
+          it 'resets the current compute minutes notification level' do
+            usage = ::Ci::Minutes::NamespaceMonthlyUsage.current_month.find_by(namespace_id: root_namespace.id)
+            usage.update!(notification_level: 30)
 
-      let(:group1) { create(:group, :with_ci_minutes, ci_minutes_used: 1600, name: 'Hello.World') }
+            expect { put namespace_path(root_namespace.id), headers: internal_api_headers, params: params }
+              .to change { usage.reload.notification_level }
+              .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set))
+          end
 
-      it 'updates namespace using full_path when full_path contains dots' do
-        put api(namespace_path(group1.full_path), admin, admin_mode: true), params: params
+          it 'refreshes cached data' do
+            expect(::Ci::Minutes::RefreshCachedDataService)
+              .to receive(:new)
+              .with(root_namespace)
+              .and_call_original
 
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit])
-        expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size])
-        expect(
-          json_response['additional_purchased_storage_ends_on']
-        ).to eq(params[:additional_purchased_storage_ends_on])
+            put namespace_path(root_namespace.id), headers: internal_api_headers, params: params
+          end
+        end
+
+        context 'when updating the shared_runners_minutes_limit' do
+          let(:params) { { shared_runners_minutes_limit: 9000 } }
+
+          it 'expires the compute minutes CachedQuota' do
+            expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!)
+
+            put namespace_path(root_namespace.id), headers: internal_api_headers, params: params
+          end
+
+          it 'resets the current compute minutes notification level' do
+            usage = ::Ci::Minutes::NamespaceMonthlyUsage.current_month.find_by(namespace_id: root_namespace.id)
+            usage.update!(notification_level: 30)
+
+            expect do
+              put namespace_path(root_namespace.id), headers: internal_api_headers, params: params
+            end.to change { usage.reload.notification_level }
+               .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set))
+          end
+        end
+
+        context 'when neither minutes_limit params is provided' do
+          let(:params) { { plan_code: 'free' } }
+
+          it 'does not expire the compute minutes CachedQuota' do
+            expect(Gitlab::Ci::Minutes::CachedQuota).not_to receive(:new)
+
+            put namespace_path(root_namespace.id), headers: internal_api_headers, params: params
+          end
+
+          it 'does not reset the current compute minutes notification level' do
+            usage = ::Ci::Minutes::NamespaceMonthlyUsage.current_month.find_by(namespace_id: root_namespace.id)
+            usage.update!(notification_level: 30)
+
+            expect { put namespace_path(root_namespace.id), headers: internal_api_headers, params: params }
+              .not_to change { usage.reload.notification_level }
+          end
+        end
       end
+    end
 
-      it 'updates namespace using id' do
-        request
+    # this method of authentication is deprecated and will be removed in
+    # https://gitlab.com/gitlab-org/gitlab/-/issues/473625
+    context 'when authenticating with an admin personal access token' do
+      let_it_be(:admin) { create(:admin) }
+      let(:user) { create(:user) }
 
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit])
-        expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size])
-        expect(
-          json_response['additional_purchased_storage_ends_on']
-        ).to eq(params[:additional_purchased_storage_ends_on])
+      let(:group1) { create(:group, :with_ci_minutes, ci_minutes_used: 1600) }
+      let_it_be(:group2) { create(:group, :nested) }
+      let_it_be(:ultimate_plan) { create(:ultimate_plan) }
+      let_it_be(:project) { create(:project, namespace: group2, name: group2.name, path: group2.path) }
+      let_it_be(:project_namespace) { project.project_namespace }
+
+      let(:usage) do
+        ::Ci::Minutes::NamespaceMonthlyUsage.current_month.find_by(namespace_id: group1)
       end
 
-      it 'expires the compute minutes CachedQuota' do
-        expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!)
+      let(:params) do
+        {
+          shared_runners_minutes_limit: 9001,
+          additional_purchased_storage_size: 10_000,
+          additional_purchased_storage_ends_on: Date.today.to_s
+        }
+      end
 
-        request
+      before do
+        usage.update!(notification_level: 30)
+        group1.update!(shared_runners_minutes_limit: 1000, extra_shared_runners_minutes_limit: 500)
       end
 
-      context 'when current compute minutes notification level is set' do
-        it 'resets the current compute minutes notification level' do
-          expect do
-            request
-          end.to change { usage.reload.notification_level }
-             .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set))
+      def namespace_path(namespace_id)
+        "/internal/gitlab_subscriptions/namespaces/#{namespace_id}"
+      end
+
+      context 'when the user is not an admin' do
+        it 'returns an error response' do
+          user = create(:user)
+
+          put api(namespace_path(group1.id), user)
+
+          expect(response).to have_gitlab_http_status(:forbidden)
         end
       end
 
-      shared_examples 'handles monthly usage' do
-        it 'expires the compute minutes CachedQuota' do
-          expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!)
+      context 'when the admin is not in admin mode' do
+        it 'returns an error response' do
+          put api(namespace_path(group1.id), admin, admin_mode: false)
 
-          request
+          expect(response).to have_gitlab_http_status(:forbidden)
         end
+      end
 
-        it 'resets the current compute minutes notification level' do
-          expect do
-            request
-          end.to change { usage.reload.notification_level }
-            .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set))
+      context 'when the namespace cannot be found' do
+        it 'returns an error response' do
+          put api(namespace_path(non_existing_record_id), admin, admin_mode: true)
+
+          expect(response).to have_gitlab_http_status(:not_found)
         end
       end
 
-      context 'when request has extra_shared_runners_minutes_limit param' do
-        before do
-          params[:extra_shared_runners_minutes_limit] = 1000
-          params.delete(:shared_runners_minutes_limit)
+      context 'when authenticated as admin' do
+        subject(:request) do
+          put api(namespace_path(group1.id), admin, admin_mode: true), params: params
         end
 
-        it 'updates the extra shared runners minutes limit' do
-          request
+        let(:group1) { create(:group, :with_ci_minutes, ci_minutes_used: 1600, name: 'Hello.World') }
+
+        it 'updates namespace using full_path when full_path contains dots' do
+          put api(namespace_path(group1.full_path), admin, admin_mode: true), params: params
 
           expect(response).to have_gitlab_http_status(:ok)
-          expect(json_response['extra_shared_runners_minutes_limit'])
-            .to eq(params[:extra_shared_runners_minutes_limit])
+          expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit])
+          expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size])
+          expect(
+            json_response['additional_purchased_storage_ends_on']
+          ).to eq(params[:additional_purchased_storage_ends_on])
         end
 
-        it 'updates pending builds data since adding extra minutes the quota is not used up anymore' do
-          minutes_exceeded = group1.ci_minutes_usage.minutes_used_up?
-          expect(minutes_exceeded).to eq(true)
-
-          pending_build = create(:ci_pending_build, namespace: group1, minutes_exceeded: minutes_exceeded)
-
+        it 'updates namespace using id' do
           request
 
-          expect(pending_build.reload.minutes_exceeded).to eq(false)
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit])
+          expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size])
+          expect(
+            json_response['additional_purchased_storage_ends_on']
+          ).to eq(params[:additional_purchased_storage_ends_on])
         end
 
-        it_behaves_like 'handles monthly usage'
-      end
+        it 'expires the compute minutes CachedQuota' do
+          expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!)
 
-      context 'when shared_runners_minutes_limit param is present' do
-        before do
-          params[:shared_runners_minutes_limit] = nil
+          request
         end
 
-        it_behaves_like 'handles monthly usage'
-      end
+        context 'when current compute minutes notification level is set' do
+          it 'resets the current compute minutes notification level' do
+            expect do
+              request
+            end.to change { usage.reload.notification_level }
+               .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set))
+          end
+        end
 
-      context 'when neither minutes limit params is provided' do
-        it 'does not expire the compute minutes CachedQuota' do
-          params.delete(:shared_runners_minutes_limit)
-          expect(Gitlab::Ci::Minutes::CachedQuota).not_to receive(:new)
+        shared_examples 'handles monthly usage' do
+          it 'expires the compute minutes CachedQuota' do
+            expect_next(Gitlab::Ci::Minutes::CachedQuota).to receive(:expire!)
 
-          request
+            request
+          end
+
+          it 'resets the current compute minutes notification level' do
+            expect do
+              request
+            end.to change { usage.reload.notification_level }
+              .to(Ci::Minutes::Notification::PERCENTAGES.fetch(:not_set))
+          end
         end
 
-        context 'when current compute minutes notification level is set' do
-          it 'does not reset the current compute minutes notification level' do
+        context 'when request has extra_shared_runners_minutes_limit param' do
+          before do
+            params[:extra_shared_runners_minutes_limit] = 1000
             params.delete(:shared_runners_minutes_limit)
+          end
 
-            expect do
-              put api(namespace_path(group1.id), admin), params: params
-            end.not_to change { usage.reload.notification_level }
+          it 'updates the extra shared runners minutes limit' do
+            request
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(json_response['extra_shared_runners_minutes_limit'])
+              .to eq(params[:extra_shared_runners_minutes_limit])
           end
+
+          it 'updates pending builds data since adding extra minutes the quota is not used up anymore' do
+            minutes_exceeded = group1.ci_minutes_usage.minutes_used_up?
+            expect(minutes_exceeded).to eq(true)
+
+            pending_build = create(:ci_pending_build, namespace: group1, minutes_exceeded: minutes_exceeded)
+
+            request
+
+            expect(pending_build.reload.minutes_exceeded).to eq(false)
+          end
+
+          it_behaves_like 'handles monthly usage'
         end
-      end
-    end
 
-    context 'when project namespace is passed' do
-      it 'returns 404' do
-        put api(namespace_path(project_namespace.id), admin, admin_mode: true), params: params
+        context 'when shared_runners_minutes_limit param is present' do
+          before do
+            params[:shared_runners_minutes_limit] = nil
+          end
 
-        expect(response).to have_gitlab_http_status(:not_found)
-        expect(json_response).to eq('message' => '404 Namespace Not Found')
-      end
-    end
+          it_behaves_like 'handles monthly usage'
+        end
 
-    context 'when invalid params' do
-      where(:attr) do
-        [
-          :shared_runners_minutes_limit,
-          :additional_purchased_storage_size,
-          :additional_purchased_storage_ends_on
-        ]
-      end
+        context 'when neither minutes limit params is provided' do
+          it 'does not expire the compute minutes CachedQuota' do
+            params.delete(:shared_runners_minutes_limit)
+            expect(Gitlab::Ci::Minutes::CachedQuota).not_to receive(:new)
+
+            request
+          end
 
-      with_them do
-        it "returns validation error for #{attr}" do
-          put api(namespace_path(group1.id), admin, admin_mode: true), params: Hash[attr, 'unknown']
+          context 'when current compute minutes notification level is set' do
+            it 'does not reset the current compute minutes notification level' do
+              params.delete(:shared_runners_minutes_limit)
 
-          expect(response).to have_gitlab_http_status(:bad_request)
+              expect { put api(namespace_path(group1.id), admin), params: params }
+                .not_to change { usage.reload.notification_level }
+            end
+          end
         end
       end
-    end
 
-    [:last_ci_minutes_notification_at, :last_ci_minutes_usage_notification_level].each do |attr|
-      context "when namespace has a value for #{attr}" do
-        before do
-          group1.update_attribute(attr, Time.now)
-        end
+      context 'when project namespace is passed' do
+        it 'returns 404' do
+          put api(namespace_path(project_namespace.id), admin, admin_mode: true), params: params
 
-        it 'resets that value when assigning extra compute minutes' do
-          expect do
-            put api(namespace_path(group1.full_path), admin, admin_mode: true),
-              params: { extra_shared_runners_minutes_limit: 1000 }
-          end.to change { group1.reload.send(attr) }.to(nil)
+          expect(response).to have_gitlab_http_status(:not_found)
+          expect(json_response).to eq('message' => '404 Namespace Not Found')
         end
       end
-    end
 
-    context "when customer purchases extra compute minutes" do
-      it "ticks instance runners" do
-        runners = Ci::Runner.instance_type
+      context 'when invalid params' do
+        where(:attr) do
+          [
+            :shared_runners_minutes_limit,
+            :additional_purchased_storage_size,
+            :additional_purchased_storage_ends_on
+          ]
+        end
 
-        put api(namespace_path(group1.id), admin), params: { extra_shared_runners_minutes_limit: 1000 }
+        with_them do
+          it "returns validation error for #{attr}" do
+            put api(namespace_path(group1.id), admin, admin_mode: true), params: Hash[attr, 'unknown']
 
-        expect(runners).to all(receive(:tick_runner_queue))
+            expect(response).to have_gitlab_http_status(:bad_request)
+          end
+        end
       end
-    end
 
-    context "when passing attributes for gitlab_subscription", :saas do
-      let(:gitlab_subscription) do
-        {
-          start_date: '2019-06-01',
-          end_date: '2020-06-01',
-          plan_code: 'ultimate',
-          seats: 20,
-          max_seats_used: 10,
-          auto_renew: true,
-          trial: true,
-          trial_ends_on: '2019-05-01',
-          trial_starts_on: '2019-06-01',
-          trial_extension_type: GitlabSubscription.trial_extension_types[:reactivated]
-        }
+      [:last_ci_minutes_notification_at, :last_ci_minutes_usage_notification_level].each do |attr|
+        context "when namespace has a value for #{attr}" do
+          before do
+            group1.update_attribute(attr, Time.now)
+          end
+
+          it 'resets that value when assigning extra compute minutes' do
+            expect do
+              put api(namespace_path(group1.id), admin, admin_mode: true),
+                params: { extra_shared_runners_minutes_limit: 1000 }
+            end.to change { group1.reload.send(attr) }.to(nil)
+          end
+        end
       end
 
-      it "creates the gitlab_subscription record" do
-        expect(group1.gitlab_subscription).to be_nil
+      context "when customer purchases extra compute minutes" do
+        it "ticks instance runners" do
+          runners = Ci::Runner.instance_type
 
-        put api(namespace_path(group1.id), admin, admin_mode: true), params: {
-          gitlab_subscription_attributes: gitlab_subscription
-        }
+          put api(namespace_path(group1.id), admin), params: { extra_shared_runners_minutes_limit: 1000 }
 
-        expect(group1.reload.gitlab_subscription).to have_attributes(
-          start_date: Date.parse(gitlab_subscription[:start_date]),
-          end_date: Date.parse(gitlab_subscription[:end_date]),
-          hosted_plan: instance_of(Plan),
-          seats: 20,
-          max_seats_used: 10,
-          auto_renew: true,
-          trial: true,
-          trial_starts_on: Date.parse(gitlab_subscription[:trial_starts_on]),
-          trial_ends_on: Date.parse(gitlab_subscription[:trial_ends_on]),
-          trial_extension_type: 'reactivated'
-        )
+          expect(runners).to all(receive(:tick_runner_queue))
+        end
       end
 
-      it "updates the gitlab_subscription record" do
-        existing_subscription = group1.create_gitlab_subscription!
-
-        put api(namespace_path(group1.id), admin, admin_mode: true), params: {
-          gitlab_subscription_attributes: gitlab_subscription
-        }
+      context "when passing attributes for gitlab_subscription", :saas do
+        let(:gitlab_subscription) do
+          {
+            start_date: '2019-06-01',
+            end_date: '2020-06-01',
+            plan_code: 'ultimate',
+            seats: 20,
+            max_seats_used: 10,
+            auto_renew: true,
+            trial: true,
+            trial_ends_on: '2019-05-01',
+            trial_starts_on: '2019-06-01',
+            trial_extension_type: GitlabSubscription.trial_extension_types[:reactivated]
+          }
+        end
 
-        expect(group1.reload.gitlab_subscription.reload.seats).to eq 20
-        expect(group1.gitlab_subscription).to eq existing_subscription
-      end
+        it "creates the gitlab_subscription record" do
+          expect(group1.gitlab_subscription).to be_nil
 
-      context 'when params are invalid' do
-        it 'returns a 400 error' do
           put api(namespace_path(group1.id), admin, admin_mode: true), params: {
-            gitlab_subscription_attributes: { start_date: nil, seats: nil }
+            gitlab_subscription_attributes: gitlab_subscription
           }
 
-          expect(response).to have_gitlab_http_status(:bad_request)
-          expect(json_response['message']).to eq(
-            "gitlab_subscription.seats" => ["can't be blank"],
-            "gitlab_subscription.start_date" => ["can't be blank"]
+          expect(group1.reload.gitlab_subscription).to have_attributes(
+            start_date: Date.parse(gitlab_subscription[:start_date]),
+            end_date: Date.parse(gitlab_subscription[:end_date]),
+            hosted_plan: instance_of(Plan),
+            seats: 20,
+            max_seats_used: 10,
+            auto_renew: true,
+            trial: true,
+            trial_starts_on: Date.parse(gitlab_subscription[:trial_starts_on]),
+            trial_ends_on: Date.parse(gitlab_subscription[:trial_ends_on]),
+            trial_extension_type: 'reactivated'
           )
         end
+
+        it "updates the gitlab_subscription record" do
+          existing_subscription = group1.create_gitlab_subscription!
+
+          put api(namespace_path(group1.id), admin, admin_mode: true), params: {
+            gitlab_subscription_attributes: gitlab_subscription
+          }
+
+          expect(group1.reload.gitlab_subscription.reload.seats).to eq 20
+          expect(group1.gitlab_subscription).to eq existing_subscription
+        end
+
+        context 'when params are invalid' do
+          it 'returns a 400 error' do
+            put api(namespace_path(group1.id), admin, admin_mode: true), params: {
+              gitlab_subscription_attributes: { start_date: nil, seats: nil }
+            }
+
+            expect(response).to have_gitlab_http_status(:bad_request)
+            expect(json_response['message']).to eq(
+              "gitlab_subscription.seats" => ["can't be blank"],
+              "gitlab_subscription.start_date" => ["can't be blank"]
+            )
+          end
+        end
       end
     end
   end
diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/subscriptions_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/subscriptions_spec.rb
index a3d4c3dee4054d6932a7d507b76c362ee3625abc..e333dba3c8380266f75912aafa9054d8944f1127 100644
--- a/ee/spec/requests/gitlab_subscriptions/api/internal/subscriptions_spec.rb
+++ b/ee/spec/requests/gitlab_subscriptions/api/internal/subscriptions_spec.rb
@@ -4,114 +4,209 @@
 
 RSpec.describe GitlabSubscriptions::API::Internal::Subscriptions, :aggregate_failures, :api, feature_category: :plan_provisioning do
   describe 'GET /internal/gitlab_subscriptions/namespaces/:id/gitlab_subscription', :saas do
-    let_it_be(:admin) { create(:admin) }
+    include GitlabSubscriptions::InternalApiHelpers
+
     let_it_be(:namespace) { create(:group) }
 
     def subscription_path(namespace_id)
-      "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/gitlab_subscription"
+      internal_api("namespaces/#{namespace_id}/gitlab_subscription")
     end
 
     context 'when unauthenticated' do
       it 'returns an error response' do
-        get api(subscription_path(namespace.id))
+        get subscription_path(namespace.id)
 
         expect(response).to have_gitlab_http_status(:unauthorized)
       end
     end
 
-    context 'when the user is not an admin' do
-      it 'returns an error response' do
-        user = create(:user)
+    context 'when authenticated as the subscription portal' do
+      before do
+        stub_internal_api_authentication
+      end
 
-        get api(subscription_path(namespace.id), user)
+      context 'when the namespace cannot be found' do
+        it 'returns an error response' do
+          get subscription_path(non_existing_record_id), headers: internal_api_headers
 
-        expect(response).to have_gitlab_http_status(:forbidden)
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
       end
-    end
 
-    context 'when the admin is not in admin mode' do
-      it 'returns an error response' do
-        get api(subscription_path(namespace.id), admin, admin_mode: false)
+      context 'when the namespace does not have a subscription' do
+        it 'returns an empty response' do
+          get subscription_path(namespace.id), headers: internal_api_headers
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response.keys).to match_array(%w[plan usage billing])
+
+          expect(json_response['plan']).to eq(
+            'name' => nil,
+            'code' => nil,
+            'auto_renew' => nil,
+            'trial' => nil,
+            'upgradable' => nil,
+            'exclude_guests' => nil
+          )
+
+          expect(json_response['usage']).to eq(
+            'max_seats_used' => nil,
+            'seats_in_subscription' => nil,
+            'seats_in_use' => nil,
+            'seats_owed' => nil
+          )
+
+          expect(json_response['billing']).to eq(
+            'subscription_start_date' => nil,
+            'subscription_end_date' => nil,
+            'trial_ends_on' => nil
+          )
+        end
+      end
 
-        expect(response).to have_gitlab_http_status(:forbidden)
+      context 'when the request is authenticated for a namespace with a subscription' do
+        it 'returns the subscription data' do
+          subscription = create(
+            :gitlab_subscription,
+            :ultimate,
+            namespace: namespace,
+            auto_renew: true,
+            max_seats_used: 5
+          )
+
+          get subscription_path(namespace.id), headers: internal_api_headers
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response.keys).to match_array(%w[plan usage billing])
+
+          expect(json_response['plan']).to eq(
+            'name' => 'Ultimate',
+            'code' => 'ultimate',
+            'auto_renew' => true,
+            'trial' => false,
+            'upgradable' => false,
+            'exclude_guests' => true
+          )
+
+          expect(json_response['usage']).to eq(
+            'max_seats_used' => 5,
+            'seats_in_subscription' => 10,
+            'seats_in_use' => 0,
+            'seats_owed' => 0
+          )
+
+          expect(json_response['billing']).to eq(
+            'subscription_start_date' => subscription.start_date.iso8601,
+            'subscription_end_date' => subscription.end_date.iso8601,
+            'trial_ends_on' => nil
+          )
+        end
       end
     end
 
-    context 'when the namespace cannot be found' do
-      it 'returns an error response' do
-        get api(subscription_path(non_existing_record_id), admin, admin_mode: true)
+    # this method of authentication is deprecated and will be removed in
+    # https://gitlab.com/gitlab-org/gitlab/-/issues/473625
+    context 'when authenticating with an admin personal access token' do
+      let_it_be(:admin) { create(:admin) }
 
-        expect(response).to have_gitlab_http_status(:not_found)
+      def subscription_path(namespace_id)
+        "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/gitlab_subscription"
       end
-    end
 
-    context 'when the namespace does not have a subscription' do
-      it 'returns an empty response' do
-        get api(subscription_path(namespace.id), admin, admin_mode: true)
-
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response.keys).to match_array(%w[plan usage billing])
-
-        expect(json_response['plan']).to eq(
-          'name' => nil,
-          'code' => nil,
-          'auto_renew' => nil,
-          'trial' => nil,
-          'upgradable' => nil,
-          'exclude_guests' => nil
-        )
-
-        expect(json_response['usage']).to eq(
-          'max_seats_used' => nil,
-          'seats_in_subscription' => nil,
-          'seats_in_use' => nil,
-          'seats_owed' => nil
-        )
-
-        expect(json_response['billing']).to eq(
-          'subscription_start_date' => nil,
-          'subscription_end_date' => nil,
-          'trial_ends_on' => nil
-        )
+      context 'when the user is not an admin' do
+        it 'returns an error response' do
+          user = create(:user)
+
+          get api(subscription_path(namespace.id), user)
+
+          expect(response).to have_gitlab_http_status(:forbidden)
+        end
+      end
+
+      context 'when the admin is not in admin mode' do
+        it 'returns an error response' do
+          get api(subscription_path(namespace.id), admin, admin_mode: false)
+
+          expect(response).to have_gitlab_http_status(:forbidden)
+        end
+      end
+
+      context 'when the namespace cannot be found' do
+        it 'returns an error response' do
+          get api(subscription_path(non_existing_record_id), admin, admin_mode: true)
+
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
+      end
+
+      context 'when the namespace does not have a subscription' do
+        it 'returns an empty response' do
+          get api(subscription_path(namespace.id), admin, admin_mode: true)
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response.keys).to match_array(%w[plan usage billing])
+
+          expect(json_response['plan']).to eq(
+            'name' => nil,
+            'code' => nil,
+            'auto_renew' => nil,
+            'trial' => nil,
+            'upgradable' => nil,
+            'exclude_guests' => nil
+          )
+
+          expect(json_response['usage']).to eq(
+            'max_seats_used' => nil,
+            'seats_in_subscription' => nil,
+            'seats_in_use' => nil,
+            'seats_owed' => nil
+          )
+
+          expect(json_response['billing']).to eq(
+            'subscription_start_date' => nil,
+            'subscription_end_date' => nil,
+            'trial_ends_on' => nil
+          )
+        end
       end
-    end
 
-    context 'when the request is authenticated for a namespace with a subscription' do
-      it 'returns the subscription data' do
-        subscription = create(
-          :gitlab_subscription,
-          :ultimate,
-          namespace: namespace,
-          auto_renew: true,
-          max_seats_used: 5
-        )
-
-        get api(subscription_path(namespace.id), admin, admin_mode: true)
-
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response.keys).to match_array(%w[plan usage billing])
-
-        expect(json_response['plan']).to eq(
-          'name' => 'Ultimate',
-          'code' => 'ultimate',
-          'auto_renew' => true,
-          'trial' => false,
-          'upgradable' => false,
-          'exclude_guests' => true
-        )
-
-        expect(json_response['usage']).to eq(
-          'max_seats_used' => 5,
-          'seats_in_subscription' => 10,
-          'seats_in_use' => 0,
-          'seats_owed' => 0
-        )
-
-        expect(json_response['billing']).to eq(
-          'subscription_start_date' => subscription.start_date.iso8601,
-          'subscription_end_date' => subscription.end_date.iso8601,
-          'trial_ends_on' => nil
-        )
+      context 'when the request is authenticated for a namespace with a subscription' do
+        it 'returns the subscription data' do
+          subscription = create(
+            :gitlab_subscription,
+            :ultimate,
+            namespace: namespace,
+            auto_renew: true,
+            max_seats_used: 5
+          )
+
+          get api(subscription_path(namespace.id), admin, admin_mode: true)
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response.keys).to match_array(%w[plan usage billing])
+
+          expect(json_response['plan']).to eq(
+            'name' => 'Ultimate',
+            'code' => 'ultimate',
+            'auto_renew' => true,
+            'trial' => false,
+            'upgradable' => false,
+            'exclude_guests' => true
+          )
+
+          expect(json_response['usage']).to eq(
+            'max_seats_used' => 5,
+            'seats_in_subscription' => 10,
+            'seats_in_use' => 0,
+            'seats_owed' => 0
+          )
+
+          expect(json_response['billing']).to eq(
+            'subscription_start_date' => subscription.start_date.iso8601,
+            'subscription_end_date' => subscription.end_date.iso8601,
+            'trial_ends_on' => nil
+          )
+        end
       end
     end
   end
diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/upcoming_reconciliations_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/upcoming_reconciliations_spec.rb
index a5013cd1df6085c9972e47eb3155135e908db853..fada96d84f4e7b8412653f54e51a1dfe899e64e1 100644
--- a/ee/spec/requests/gitlab_subscriptions/api/internal/upcoming_reconciliations_spec.rb
+++ b/ee/spec/requests/gitlab_subscriptions/api/internal/upcoming_reconciliations_spec.rb
@@ -3,90 +3,137 @@
 require 'spec_helper'
 
 RSpec.describe GitlabSubscriptions::API::Internal::UpcomingReconciliations, :aggregate_failures, :api, feature_category: :subscription_management do
+  include GitlabSubscriptions::InternalApiHelpers
+
   before do
     stub_saas_features(gitlab_com_subscriptions: true)
     stub_application_setting(check_namespace_plan: true)
   end
 
+  def upcoming_reconciliations_path(namespace_id)
+    internal_api("namespaces/#{namespace_id}/upcoming_reconciliations")
+  end
+
   describe 'PUT /internal/gitlab_subscriptions/namespaces/:namespace_id/upcoming_reconciliations' do
+    let_it_be(:namespace) { create(:group) }
+
     context 'when unauthenticated' do
       it 'returns authentication error' do
-        put api('/internal/gitlab_subscriptions/namespaces/1/upcoming_reconciliations')
+        put upcoming_reconciliations_path(namespace.id)
 
         expect(response).to have_gitlab_http_status(:unauthorized)
       end
     end
 
-    context 'when authenticated as user' do
-      let_it_be(:user) { create(:user) }
+    context 'when authenticated as the subscription portal' do
+      before do
+        stub_internal_api_authentication
+      end
 
-      it 'returns authentication error' do
-        put api('/internal/gitlab_subscriptions/namespaces/1/upcoming_reconciliations', user)
+      context 'when supplied valid params' do
+        it 'updates the upcoming reconciliation' do
+          params = {
+            next_reconciliation_date: Date.today + 5.days,
+            display_alert_from: Date.today - 2.days
+          }
 
-        expect(response).to have_gitlab_http_status(:forbidden)
+          expect { put upcoming_reconciliations_path(namespace.id), headers: internal_api_headers, params: params }
+            .to change { namespace.reload.upcoming_reconciliation }
+            .to be_present
+
+          expect(response).to have_gitlab_http_status(:ok)
+        end
+      end
+
+      context 'when supplied invalid params' do
+        it 'returns an error' do
+          params = {
+            next_reconciliation_date: nil,
+            display_alert_from: Date.today - 2.days
+          }
+
+          put upcoming_reconciliations_path(namespace.id), headers: internal_api_headers, params: params
+
+          expect(response).to have_gitlab_http_status(:internal_server_error)
+          expect(json_response['message']['error']).to include "Next reconciliation date can't be blank"
+        end
       end
     end
 
-    context 'when authenticated as admin' do
-      let_it_be(:default_organization) { create(:organization, :default) }
-      let_it_be(:admin) { create(:admin) }
-      let_it_be(:namespace) { create(:namespace) }
-      let(:namespace_id) { namespace.id }
+    # this method of authentication is deprecated and will be removed in
+    # https://gitlab.com/gitlab-org/gitlab/-/issues/473625
+    context 'when authenticating with an admin personal access token' do
       let(:path) { "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/upcoming_reconciliations" }
+      let(:namespace_id) { namespace.id }
+
+      context 'when authenticated as user' do
+        let_it_be(:user) { create(:user) }
+
+        it 'returns authentication error' do
+          put api(path, user)
 
-      let(:params) do
-        {
-          next_reconciliation_date: Date.today + 5.days,
-          display_alert_from: Date.today - 2.days
-        }
+          expect(response).to have_gitlab_http_status(:forbidden)
+        end
       end
 
-      it_behaves_like 'PUT request permissions for admin mode' do
+      context 'when authenticated as admin' do
+        let_it_be(:default_organization) { create(:organization, :default) }
+        let_it_be(:admin) { create(:admin) }
+
         let(:params) do
           {
             next_reconciliation_date: Date.today + 5.days,
             display_alert_from: Date.today - 2.days
           }
         end
-      end
 
-      subject(:put_upcoming_reconciliations) do
-        put api(path, admin, admin_mode: true), params: params
-      end
+        it_behaves_like 'PUT request permissions for admin mode' do
+          let(:params) do
+            {
+              next_reconciliation_date: Date.today + 5.days,
+              display_alert_from: Date.today - 2.days
+            }
+          end
+        end
 
-      it 'returns success' do
-        put_upcoming_reconciliations
+        subject(:put_upcoming_reconciliations) do
+          put api(path, admin, admin_mode: true), params: params
+        end
 
-        expect(response).to have_gitlab_http_status(:ok)
-      end
+        it 'returns success' do
+          put_upcoming_reconciliations
 
-      context 'when update service failed' do
-        let(:error_message) { 'update_service_error' }
+          expect(response).to have_gitlab_http_status(:ok)
+        end
 
-        before do
-          allow_next_instance_of(::UpcomingReconciliations::UpdateService) do |service|
-            allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message))
+        context 'when update service failed' do
+          let(:error_message) { 'update_service_error' }
+
+          before do
+            allow_next_instance_of(::UpcomingReconciliations::UpdateService) do |service|
+              allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message))
+            end
           end
-        end
 
-        it 'returns error' do
-          put_upcoming_reconciliations
+          it 'returns error' do
+            put_upcoming_reconciliations
 
-          expect(response).to have_gitlab_http_status(:internal_server_error)
-          expect(json_response.dig('message', 'error')).to eq(error_message)
+            expect(response).to have_gitlab_http_status(:internal_server_error)
+            expect(json_response.dig('message', 'error')).to eq(error_message)
+          end
         end
-      end
 
-      context 'when not gitlab.com' do
-        before do
-          stub_saas_features(gitlab_com_subscriptions: false)
-        end
+        context 'when not gitlab.com' do
+          before do
+            stub_saas_features(gitlab_com_subscriptions: false)
+          end
 
-        it 'returns 403 error' do
-          put_upcoming_reconciliations
+          it 'returns 403 error' do
+            put_upcoming_reconciliations
 
-          expect(response).to have_gitlab_http_status(:forbidden)
-          expect(json_response['message']).to eq('403 Forbidden - This API is gitlab.com only!')
+            expect(response).to have_gitlab_http_status(:forbidden)
+            expect(json_response['message']).to eq('403 Forbidden - This API is gitlab.com only!')
+          end
         end
       end
     end
@@ -94,68 +141,103 @@
 
   describe 'DELETE /internal/gitlab_subscriptions/namespaces/:namespace_id/upcoming_reconciliations' do
     let_it_be(:namespace) { create(:namespace) }
-    let(:path) { "/internal/gitlab_subscriptions/namespaces/#{namespace.id}/upcoming_reconciliations" }
 
-    it_behaves_like 'DELETE request permissions for admin mode' do
-      before do
-        create(:upcoming_reconciliation, namespace_id: namespace.id)
-      end
-    end
-
-    context 'when the request is not authenticated' do
+    context 'when unauthenticated' do
       it 'returns authentication error' do
-        delete api(path)
+        delete upcoming_reconciliations_path(namespace.id)
 
         expect(response).to have_gitlab_http_status(:unauthorized)
       end
     end
 
-    context 'when authenticated as user' do
-      it 'returns authentication error' do
-        user = create(:user)
+    context 'when authenticated as the subscription portal' do
+      subject(:delete_upcoming_reconciliation) do
+        delete upcoming_reconciliations_path(namespace.id), headers: internal_api_headers
+      end
 
-        expect { delete api(path, user) }
-          .not_to change { GitlabSubscriptions::UpcomingReconciliation.count }
+      before do
+        stub_internal_api_authentication
+      end
+
+      context 'when there is an upcoming reconciliation for the namespace' do
+        it 'destroys the reconciliation and returns success' do
+          create(:upcoming_reconciliation, namespace_id: namespace.id)
 
-        expect(response).to have_gitlab_http_status(:forbidden)
+          expect { delete_upcoming_reconciliation }
+            .to change { ::GitlabSubscriptions::UpcomingReconciliation.where(namespace_id: namespace.id).count }
+            .by(-1)
+
+          expect(response).to have_gitlab_http_status(:no_content)
+        end
+      end
+
+      context 'when the namespace_id does not have an upcoming reconciliation' do
+        it 'returns a not found error' do
+          expect { delete_upcoming_reconciliation }.not_to change { GitlabSubscriptions::UpcomingReconciliation.count }
+
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
       end
     end
 
-    context 'when authenticated as an admin' do
-      let_it_be(:admin) { create(:admin) }
+    # this method of authentication is deprecated and will be removed in
+    # https://gitlab.com/gitlab-org/gitlab/-/issues/473625
+    context 'when authenticating with an admin personal access token' do
+      let(:path) { "/internal/gitlab_subscriptions/namespaces/#{namespace.id}/upcoming_reconciliations" }
 
-      context 'when the request is not for .com' do
+      it_behaves_like 'DELETE request permissions for admin mode' do
         before do
-          stub_saas_features(gitlab_com_subscriptions: false)
+          create(:upcoming_reconciliation, namespace_id: namespace.id)
         end
+      end
 
-        it 'returns an error' do
-          expect { delete api(path, admin, admin_mode: true) }
+      context 'when authenticated as user' do
+        it 'returns authentication error' do
+          user = create(:user)
+
+          expect { delete api(path, user) }
             .not_to change { GitlabSubscriptions::UpcomingReconciliation.count }
 
           expect(response).to have_gitlab_http_status(:forbidden)
-          expect(response.body).to include('403 Forbidden - This API is gitlab.com only!')
         end
       end
 
-      context 'when there is an upcoming reconciliation for the namespace' do
-        it 'destroys the reconciliation and returns success' do
-          create(:upcoming_reconciliation, namespace_id: namespace.id)
+      context 'when authenticated as an admin' do
+        let_it_be(:admin) { create(:admin) }
 
-          expect { delete api(path, admin, admin_mode: true) }
-            .to change { ::GitlabSubscriptions::UpcomingReconciliation.where(namespace_id: namespace.id).count }
-            .by(-1)
+        context 'when the request is not for .com' do
+          before do
+            stub_saas_features(gitlab_com_subscriptions: false)
+          end
 
-          expect(response).to have_gitlab_http_status(:no_content)
+          it 'returns an error' do
+            expect { delete api(path, admin, admin_mode: true) }
+              .not_to change { GitlabSubscriptions::UpcomingReconciliation.count }
+
+            expect(response).to have_gitlab_http_status(:forbidden)
+            expect(response.body).to include('403 Forbidden - This API is gitlab.com only!')
+          end
         end
-      end
 
-      context 'when the namespace_id does not have an upcoming reconciliation' do
-        it 'returns a not found error' do
-          expect { delete api(path, admin, admin_mode: true) }
-            .not_to change { GitlabSubscriptions::UpcomingReconciliation.count }
+        context 'when there is an upcoming reconciliation for the namespace' do
+          it 'destroys the reconciliation and returns success' do
+            create(:upcoming_reconciliation, namespace_id: namespace.id)
 
-          expect(response).to have_gitlab_http_status(:not_found)
+            expect { delete api(path, admin, admin_mode: true) }
+              .to change { ::GitlabSubscriptions::UpcomingReconciliation.where(namespace_id: namespace.id).count }
+              .by(-1)
+
+            expect(response).to have_gitlab_http_status(:no_content)
+          end
+        end
+
+        context 'when the namespace_id does not have an upcoming reconciliation' do
+          it 'returns a not found error' do
+            expect { delete api(path, admin, admin_mode: true) }
+              .not_to change { GitlabSubscriptions::UpcomingReconciliation.count }
+
+            expect(response).to have_gitlab_http_status(:not_found)
+          end
         end
       end
     end
diff --git a/ee/spec/requests/gitlab_subscriptions/api/internal/users_spec.rb b/ee/spec/requests/gitlab_subscriptions/api/internal/users_spec.rb
index e5529a88d554921cb4ada9bbe2c4bf665fdb86c0..2fe00ff1fc59bf1fba01b96271a233397b996d52 100644
--- a/ee/spec/requests/gitlab_subscriptions/api/internal/users_spec.rb
+++ b/ee/spec/requests/gitlab_subscriptions/api/internal/users_spec.rb
@@ -3,53 +3,85 @@
 require 'spec_helper'
 
 RSpec.describe GitlabSubscriptions::API::Internal::Users, :aggregate_failures, :api, feature_category: :subscription_management do
+  include GitlabSubscriptions::InternalApiHelpers
+
   describe 'GET /internal/gitlab_subscriptions/users/:id' do
     let_it_be(:user) { create(:user) }
-    let(:user_id) { user.id }
-    let(:user_path) { "/internal/gitlab_subscriptions/users/#{user_id}" }
+
+    def users_path(user_id)
+      internal_api("users/#{user_id}")
+    end
 
     context 'when unauthenticated' do
       it 'returns authentication error' do
-        get api(user_path)
+        get users_path(user.id)
 
         expect(response).to have_gitlab_http_status(:unauthorized)
       end
     end
 
-    context 'when authenticated as user' do
-      it 'returns authentication error' do
-        get api(user_path, create(:user))
+    context 'when authenticated as the subscription portal' do
+      before do
+        stub_internal_api_authentication
+      end
+
+      context 'when the user exists' do
+        it 'returns success' do
+          get users_path(user.id), headers: internal_api_headers
+
+          expect(response).to have_gitlab_http_status(:ok)
+
+          expect(json_response["id"]).to eq(user.id)
+          expect(json_response.keys).to eq(%w[id username name web_url])
+        end
+      end
+
+      context 'when user does not exists' do
+        it 'returns not found' do
+          get users_path(non_existing_record_id), headers: internal_api_headers
 
-        expect(response).to have_gitlab_http_status(:forbidden)
+          expect(response).to have_gitlab_http_status(:not_found)
+          expect(json_response['message']).to eq("404 User Not Found")
+        end
       end
     end
 
-    context 'when authenticated as admin' do
-      let_it_be(:admin) { create(:admin) }
+    # this method of authentication is deprecated and will be removed in
+    # https://gitlab.com/gitlab-org/gitlab/-/issues/473625
+    context 'when authenticating with an admin personal access token' do
+      def users_path(user_id)
+        "/internal/gitlab_subscriptions/users/#{user_id}"
+      end
 
-      subject(:get_user) do
-        get api(user_path, admin, admin_mode: true)
+      context 'when authenticated as user' do
+        it 'returns authentication error' do
+          get api(users_path(user.id), create(:user))
+
+          expect(response).to have_gitlab_http_status(:forbidden)
+        end
       end
 
-      it 'returns success' do
-        get_user
+      context 'when authenticated as admin' do
+        let_it_be(:admin) { create(:admin) }
 
-        expected_attributes = %w[id username name web_url]
+        it 'returns success' do
+          get api(users_path(user.id), admin, admin_mode: true)
 
-        expect(response).to have_gitlab_http_status(:ok)
+          expected_attributes = %w[id username name web_url]
 
-        expect(json_response["id"]).to eq(user_id)
-        expect(json_response.keys).to eq(expected_attributes)
-      end
+          expect(response).to have_gitlab_http_status(:ok)
 
-      context 'when user does not exists' do
-        let(:user_id) { -1 }
+          expect(json_response["id"]).to eq(user.id)
+          expect(json_response.keys).to eq(expected_attributes)
+        end
 
-        it 'returns not found' do
-          get_user
+        context 'when user does not exists' do
+          it 'returns not found' do
+            get api(users_path(non_existing_record_id), admin, admin_mode: true)
 
-          expect(response).to have_gitlab_http_status(:not_found)
-          expect(json_response['message']).to eq("404 User Not Found")
+            expect(response).to have_gitlab_http_status(:not_found)
+            expect(json_response['message']).to eq("404 User Not Found")
+          end
         end
       end
     end
@@ -60,35 +92,27 @@
     let_it_be(:user) { create(:user) }
 
     def user_permissions_path(namespace_id, user_id)
-      "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/user_permissions/#{user_id}"
+      internal_api("namespaces/#{namespace_id}/user_permissions/#{user_id}")
     end
 
     context 'when unauthenticated' do
       it 'returns an authentication error' do
-        get api(user_permissions_path(namespace.id, user.id))
+        get user_permissions_path(namespace.id, user.id)
 
         expect(response).to have_gitlab_http_status(:unauthorized)
       end
     end
 
-    context 'when authenticated as a non-admin user' do
-      it 'returns an authentication error' do
-        non_admin = create(:user)
-
-        get api(user_permissions_path(namespace.id, user.id), non_admin)
-
-        expect(response).to have_gitlab_http_status(:forbidden)
+    context 'when authenticated as the subscription portal' do
+      before do
+        stub_internal_api_authentication
       end
-    end
-
-    context 'when authenticated as an admin' do
-      let_it_be(:admin) { create(:admin) }
 
       context 'when the user can manage the namespace billing' do
         it 'returns true for edit_billing' do
           namespace.add_owner(user)
 
-          get api(user_permissions_path(namespace.id, user.id), admin, admin_mode: true)
+          get user_permissions_path(namespace.id, user.id), headers: internal_api_headers
 
           expect(response).to have_gitlab_http_status(:ok)
           expect(json_response['edit_billing']).to be true
@@ -97,7 +121,7 @@ def user_permissions_path(namespace_id, user_id)
 
       context 'when the user cannot manage the namespace billing' do
         it 'returns false for edit_billing' do
-          get api(user_permissions_path(namespace.id, user.id), admin, admin_mode: true)
+          get user_permissions_path(namespace.id, user.id), headers: internal_api_headers
 
           expect(response).to have_gitlab_http_status(:ok)
           expect(json_response['edit_billing']).to be false
@@ -106,7 +130,7 @@ def user_permissions_path(namespace_id, user_id)
 
       context 'when the namespace does not exist' do
         it 'returns a not found response' do
-          get api(user_permissions_path(non_existing_record_id, user.id), admin, admin_mode: true)
+          get user_permissions_path(non_existing_record_id, user.id), headers: internal_api_headers
 
           expect(response).to have_gitlab_http_status(:not_found)
         end
@@ -114,11 +138,69 @@ def user_permissions_path(namespace_id, user_id)
 
       context 'when the user does not exist' do
         it 'returns a not found response' do
-          get api(user_permissions_path(namespace.id, non_existing_record_id), admin, admin_mode: true)
+          get user_permissions_path(namespace.id, non_existing_record_id), headers: internal_api_headers
 
           expect(response).to have_gitlab_http_status(:not_found)
         end
       end
     end
+
+    # this method of authentication is deprecated and will be removed in
+    # https://gitlab.com/gitlab-org/gitlab/-/issues/473625
+    context 'when authenticating with an admin personal access token' do
+      def user_permissions_path(namespace_id, user_id)
+        "/internal/gitlab_subscriptions/namespaces/#{namespace_id}/user_permissions/#{user_id}"
+      end
+
+      context 'when authenticated as a non-admin user' do
+        it 'returns an authentication error' do
+          non_admin = create(:user)
+
+          get api(user_permissions_path(namespace.id, user.id), non_admin)
+
+          expect(response).to have_gitlab_http_status(:forbidden)
+        end
+      end
+
+      context 'when authenticated as an admin' do
+        let_it_be(:admin) { create(:admin) }
+
+        context 'when the user can manage the namespace billing' do
+          it 'returns true for edit_billing' do
+            namespace.add_owner(user)
+
+            get api(user_permissions_path(namespace.id, user.id), admin, admin_mode: true)
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(json_response['edit_billing']).to be true
+          end
+        end
+
+        context 'when the user cannot manage the namespace billing' do
+          it 'returns false for edit_billing' do
+            get api(user_permissions_path(namespace.id, user.id), admin, admin_mode: true)
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(json_response['edit_billing']).to be false
+          end
+        end
+
+        context 'when the namespace does not exist' do
+          it 'returns a not found response' do
+            get api(user_permissions_path(non_existing_record_id, user.id), admin, admin_mode: true)
+
+            expect(response).to have_gitlab_http_status(:not_found)
+          end
+        end
+
+        context 'when the user does not exist' do
+          it 'returns a not found response' do
+            get api(user_permissions_path(namespace.id, non_existing_record_id), admin, admin_mode: true)
+
+            expect(response).to have_gitlab_http_status(:not_found)
+          end
+        end
+      end
+    end
   end
 end
diff --git a/spec/support/helpers/gitlab_subscriptions/internal_api_helpers.rb b/spec/support/helpers/gitlab_subscriptions/internal_api_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c8625c2416956ed7ee2940afb4f524546fe7bbe1
--- /dev/null
+++ b/spec/support/helpers/gitlab_subscriptions/internal_api_helpers.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module GitlabSubscriptions
+  module InternalApiHelpers
+    def internal_api(path)
+      "/api/#{::API::API.version}/internal/gitlab_subscriptions/#{path}"
+    end
+
+    def internal_api_headers
+      { 'X-Customers-Dot-Internal-Token' => 'internal-api-token' }
+    end
+
+    def stub_internal_api_authentication
+      allow(GitlabSubscriptions::API::Internal::Auth)
+        .to receive(:verify_api_request)
+        .with(hash_including(**internal_api_headers))
+        .and_return(['decoded-token'])
+    end
+  end
+end