diff --git a/config/feature_flags/development/pat_creation_api_for_admin.yml b/config/feature_flags/development/pat_creation_api_for_admin.yml
new file mode 100644
index 0000000000000000000000000000000000000000..246f7623cc93d016f7facbf824af08ae3b7c33a6
--- /dev/null
+++ b/config/feature_flags/development/pat_creation_api_for_admin.yml
@@ -0,0 +1,7 @@
+---
+name: pat_creation_api_for_admin
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45152
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267553
+type: development
+group: group::access
+default_enabled: false
diff --git a/doc/api/personal_access_tokens.md b/doc/api/personal_access_tokens.md
index 5bd804b80428d3c0c31716224d4e1151e8c0442c..8a838a0f1796a39b070f4df856c795bf5110adf4 100644
--- a/doc/api/personal_access_tokens.md
+++ b/doc/api/personal_access_tokens.md
@@ -93,3 +93,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
 
 - `204: No Content` if successfully revoked.
 - `400 Bad Request` if not revoked successfully.
+
+## Create a personal access token (admin only)
+
+See the [Users API documentation](users.md#create-a-personal-access-token-admin-only) for information on creating a personal access token.
diff --git a/doc/api/users.md b/doc/api/users.md
index 31e8bb67bd3c54a5ca9cec1558456412acc60bf8..e1fa97765df828bf1fc5bf58898290701dca453d 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -1441,7 +1441,54 @@ Parameters:
 | `user_id`                | integer | yes      | The ID of the user                |
 | `impersonation_token_id` | integer | yes      | The ID of the impersonation token |
 
-### Get user activities (admin only)
+## Create a personal access token (admin only)
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17176) in GitLab 13.6.
+> - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-an-administrators-ability-to-use-the-api-to-create-personal-access-tokens). **(CORE)**
+
+CAUTION: **Warning:**
+This feature might not be available to you. Check the **version history** note above for details.
+
+> Requires admin permissions.
+> Token values are returned once. Make sure you save it - you won't be able to access it again.
+
+It creates a new personal access token.
+
+```plaintext
+POST /users/:user_id/personal_access_tokens
+```
+
+| Attribute    | Type    | Required | Description                                                                                                              |
+| ------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
+| `user_id`    | integer | yes      | The ID of the user                                                                                                       |
+| `name`       | string  | yes      | The name of the personal access token                                                                                    |
+| `expires_at` | date    | no       | The expiration date of the personal access token in ISO format (`YYYY-MM-DD`)                                            |
+| `scopes`     | array   | yes      | The array of scopes of the personal access token (`api`, `read_user`, `read_api`, `read_repository`, `write_repository`) |
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "name=mytoken" --data "expires_at=2017-04-04" --data "scopes[]=api" "https://gitlab.example.com/api/v4/users/42/personal_access_tokens"
+```
+
+Example response:
+
+```json
+{
+    "id": 3,
+    "name": "mytoken",
+    "revoked": false,
+    "created_at": "2020-10-14T11:58:53.526Z",
+    "scopes": [
+        "api"
+    ],
+    "user_id": 42,
+    "active": true,
+    "expires_at": "2020-12-31",
+    "token": "ggbfKkC4n-Lujy8jwCR2"
+}
+```
+
+## Get user activities (admin only)
 
 NOTE: **Note:**
 This API endpoint is only available on 8.15 (EE) and 9.1 (CE) and above.
@@ -1546,3 +1593,22 @@ Example response:
   },
 ]
 ```
+
+## Enable or disable an administrator's ability to use the API to create personal access tokens **(CORE)**
+
+An administrator's ability to create personal access tokens through the API is
+deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
+can enable it.
+
+To enable it:
+
+```ruby
+Feature.enable(:pat_creation_api_for_admin)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:pat_creation_api_for_admin)
+```
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 54228373512da43ffa803db2db72cb407206235f..501ed629c7e6c20f30eba7f6ebd0b9bef455e186 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -65,9 +65,9 @@ module API
 
         params :sort_params do
           optional :order_by, type: String, values: %w[id name username created_at updated_at],
-                              default: 'id', desc: 'Return users ordered by a field'
+            default: 'id', desc: 'Return users ordered by a field'
           optional :sort, type: String, values: %w[asc desc], default: 'desc',
-                          desc: 'Return users sorted in ascending and descending order'
+            desc: 'Return users sorted in ascending and descending order'
         end
       end
 
@@ -706,6 +706,40 @@ module API
             end
           end
         end
+
+        resource :personal_access_tokens do
+          helpers do
+            def target_user
+              find_user_by_id(params)
+            end
+          end
+
+          before { authenticated_as_admin! }
+
+          desc 'Create a personal access token. Available only for admins.' do
+            detail 'This feature was introduced in GitLab 13.6'
+            success Entities::PersonalAccessTokenWithToken
+          end
+          params do
+            requires :name, type: String, desc: 'The name of the personal access token'
+            requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::Gitlab::Auth.all_available_scopes.map(&:to_s),
+              desc: 'The array of scopes of the personal access token'
+            optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token'
+          end
+          post feature_category: :authentication_and_authorization do
+            not_found! unless Feature.enabled?(:pat_creation_api_for_admin)
+
+            response = ::PersonalAccessTokens::CreateService.new(
+              current_user: current_user, target_user: target_user, params: declared_params(include_missing: false)
+            ).execute
+
+            if response.success?
+              present response.payload[:personal_access_token], with: Entities::PersonalAccessTokenWithToken
+            else
+              render_api_error!(response.message, response.http_status || :unprocessable_entity)
+            end
+          end
+        end
       end
     end
 
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 7330c89fe77575433a66e41817cc45179532fb25..98840d6238acb2ccccf8e68f6de2ea8654c02772 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -161,7 +161,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
             end
 
             context 'accesses the profile of another admin' do
-              let(:admin_2) {create(:admin, note: '2010-10-10 | 2FA added | admin requested | www.gitlab.com')}
+              let(:admin_2) { create(:admin, note: '2010-10-10 | 2FA added | admin requested | www.gitlab.com') }
 
               it 'contains the note of the user' do
                 get api("/user?private_token=#{admin_personal_access_token}&sudo=#{admin_2.id}")
@@ -772,11 +772,11 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
 
     it "does not create user with invalid email" do
       post api('/users', admin),
-           params: {
-             email: 'invalid email',
-             password: 'password',
-             name: 'test'
-           }
+        params: {
+          email: 'invalid email',
+          password: 'password',
+          name: 'test'
+        }
       expect(response).to have_gitlab_http_status(:bad_request)
     end
 
@@ -811,14 +811,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
 
     it 'returns 400 error if user does not validate' do
       post api('/users', admin),
-           params: {
-             password: 'pass',
-             email: 'test@example.com',
-             username: 'test!',
-             name: 'test',
-             bio: 'g' * 256,
-             projects_limit: -1
-           }
+        params: {
+          password: 'pass',
+          email: 'test@example.com',
+          username: 'test!',
+          name: 'test',
+          bio: 'g' * 256,
+          projects_limit: -1
+        }
       expect(response).to have_gitlab_http_status(:bad_request)
       expect(json_response['message']['password'])
         .to eq(['is too short (minimum is 8 characters)'])
@@ -838,23 +838,23 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
     context 'with existing user' do
       before do
         post api('/users', admin),
-             params: {
-               email: 'test@example.com',
-               password: 'password',
-               username: 'test',
-               name: 'foo'
-             }
+          params: {
+            email: 'test@example.com',
+            password: 'password',
+            username: 'test',
+            name: 'foo'
+          }
       end
 
       it 'returns 409 conflict error if user with same email exists' do
         expect do
           post api('/users', admin),
-               params: {
-                 name: 'foo',
-                 email: 'test@example.com',
-                 password: 'password',
-                 username: 'foo'
-               }
+            params: {
+              name: 'foo',
+              email: 'test@example.com',
+              password: 'password',
+              username: 'foo'
+            }
         end.to change { User.count }.by(0)
         expect(response).to have_gitlab_http_status(:conflict)
         expect(json_response['message']).to eq('Email has already been taken')
@@ -863,12 +863,12 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
       it 'returns 409 conflict error if same username exists' do
         expect do
           post api('/users', admin),
-               params: {
-                 name: 'foo',
-                 email: 'foo@example.com',
-                 password: 'password',
-                 username: 'test'
-               }
+            params: {
+              name: 'foo',
+              email: 'foo@example.com',
+              password: 'password',
+              username: 'test'
+            }
         end.to change { User.count }.by(0)
         expect(response).to have_gitlab_http_status(:conflict)
         expect(json_response['message']).to eq('Username has already been taken')
@@ -877,12 +877,12 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
       it 'returns 409 conflict error if same username exists (case insensitive)' do
         expect do
           post api('/users', admin),
-               params: {
-                 name: 'foo',
-                 email: 'foo@example.com',
-                 password: 'password',
-                 username: 'TEST'
-               }
+            params: {
+              name: 'foo',
+              email: 'foo@example.com',
+              password: 'password',
+              username: 'TEST'
+            }
         end.to change { User.count }.by(0)
         expect(response).to have_gitlab_http_status(:conflict)
         expect(json_response['message']).to eq('Username has already been taken')
@@ -1185,14 +1185,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
 
     it 'returns 400 error if user does not validate' do
       put api("/users/#{user.id}", admin),
-          params: {
-            password: 'pass',
-            email: 'test@example.com',
-            username: 'test!',
-            name: 'test',
-            bio: 'g' * 256,
-            projects_limit: -1
-          }
+        params: {
+          password: 'pass',
+          email: 'test@example.com',
+          username: 'test!',
+          name: 'test',
+          bio: 'g' * 256,
+          projects_limit: -1
+        }
       expect(response).to have_gitlab_http_status(:bad_request)
       expect(json_response['message']['password'])
         .to eq(['is too short (minimum is 8 characters)'])
@@ -1714,14 +1714,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
 
       context "hard delete disabled" do
         it "does not delete user" do
-          perform_enqueued_jobs { delete api("/users/#{user.id}", admin)}
+          perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
           expect(response).to have_gitlab_http_status(:conflict)
         end
       end
 
       context "hard delete enabled" do
         it "delete user and group", :sidekiq_might_not_need_inline do
-          perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin)}
+          perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
           expect(response).to have_gitlab_http_status(:no_content)
           expect(Group.exists?(group.id)).to be_falsy
         end
@@ -1993,7 +1993,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
         delete api("/user/keys/#{key.id}", user)
 
         expect(response).to have_gitlab_http_status(:no_content)
-      end.to change { user.keys.count}.by(-1)
+      end.to change { user.keys.count }.by(-1)
     end
 
     it_behaves_like '412 response' do
@@ -2124,7 +2124,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
         post api("/user/gpg_keys/#{gpg_key.id}/revoke", user)
 
         expect(response).to have_gitlab_http_status(:accepted)
-      end.to change { user.gpg_keys.count}.by(-1)
+      end.to change { user.gpg_keys.count }.by(-1)
     end
 
     it 'returns 404 if key ID not found' do
@@ -2157,7 +2157,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
         delete api("/user/gpg_keys/#{gpg_key.id}", user)
 
         expect(response).to have_gitlab_http_status(:no_content)
-      end.to change { user.gpg_keys.count}.by(-1)
+      end.to change { user.gpg_keys.count }.by(-1)
     end
 
     it 'returns 404 if key ID not found' do
@@ -2279,7 +2279,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
         delete api("/user/emails/#{email.id}", user)
 
         expect(response).to have_gitlab_http_status(:no_content)
-      end.to change { user.emails.count}.by(-1)
+      end.to change { user.emails.count }.by(-1)
     end
 
     it_behaves_like '412 response' do
@@ -2756,6 +2756,124 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
     end
   end
 
+  describe 'POST /users/:user_id/personal_access_tokens' do
+    let(:name) { 'new pat' }
+    let(:expires_at) { 3.days.from_now.to_date.to_s }
+    let(:scopes) { %w(api read_user) }
+
+    context 'when feature flag is enabled' do
+      before do
+        stub_feature_flags(pat_creation_api_for_admin: true)
+      end
+
+      it 'returns error if required attributes are missing' do
+        post api("/users/#{user.id}/personal_access_tokens", admin)
+
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['error']).to eq('name is missing, scopes is missing, scopes does not have a valid value')
+      end
+
+      it 'returns a 404 error if user not found' do
+        post api("/users/#{non_existing_record_id}/personal_access_tokens", admin),
+          params: {
+            name: name,
+            scopes: scopes,
+            expires_at: expires_at
+          }
+
+        expect(response).to have_gitlab_http_status(:not_found)
+        expect(json_response['message']).to eq('404 User Not Found')
+      end
+
+      it 'returns a 401 error when not authenticated' do
+        post api("/users/#{user.id}/personal_access_tokens"),
+          params: {
+            name: name,
+            scopes: scopes,
+            expires_at: expires_at
+          }
+
+        expect(response).to have_gitlab_http_status(:unauthorized)
+        expect(json_response['message']).to eq('401 Unauthorized')
+      end
+
+      it 'returns a 403 error when authenticated as normal user' do
+        post api("/users/#{user.id}/personal_access_tokens", user),
+          params: {
+            name: name,
+            scopes: scopes,
+            expires_at: expires_at
+          }
+
+        expect(response).to have_gitlab_http_status(:forbidden)
+        expect(json_response['message']).to eq('403 Forbidden')
+      end
+
+      it 'creates a personal access token when authenticated as admin' do
+        post api("/users/#{user.id}/personal_access_tokens", admin),
+          params: {
+            name: name,
+            expires_at: expires_at,
+            scopes: scopes
+          }
+
+        expect(response).to have_gitlab_http_status(:created)
+        expect(json_response['name']).to eq(name)
+        expect(json_response['scopes']).to eq(scopes)
+        expect(json_response['expires_at']).to eq(expires_at)
+        expect(json_response['id']).to be_present
+        expect(json_response['created_at']).to be_present
+        expect(json_response['active']).to be_truthy
+        expect(json_response['revoked']).to be_falsey
+        expect(json_response['token']).to be_present
+      end
+
+      context 'when an error is thrown by the model' do
+        let!(:admin_personal_access_token) { create(:personal_access_token, user: admin) }
+        let(:error_message) { 'error message' }
+
+        before do
+          allow_next_instance_of(PersonalAccessToken) do |personal_access_token|
+            allow(personal_access_token).to receive_message_chain(:errors, :full_messages)
+                                                .and_return([error_message])
+
+            allow(personal_access_token).to receive(:save).and_return(false)
+          end
+        end
+
+        it 'returns the error' do
+          post api("/users/#{user.id}/personal_access_tokens", personal_access_token: admin_personal_access_token),
+              params: {
+                  name: name,
+                  expires_at: expires_at,
+                  scopes: scopes
+              }
+
+          expect(response).to have_gitlab_http_status(:unprocessable_entity)
+          expect(json_response['message']).to eq(error_message)
+        end
+      end
+    end
+
+    context 'when feature flag is disabled' do
+      before do
+        stub_feature_flags(pat_creation_api_for_admin: false)
+      end
+
+      it 'returns a 404' do
+        post api("/users/#{user.id}/personal_access_tokens", admin),
+          params: {
+            name: name,
+            expires_at: expires_at,
+            scopes: scopes
+          }
+
+        expect(response).to have_gitlab_http_status(:not_found)
+        expect(json_response['message']).to eq('404 Not Found')
+      end
+    end
+  end
+
   describe 'GET /users/:user_id/impersonation_tokens' do
     let_it_be(:active_personal_access_token) { create(:personal_access_token, user: user) }
     let_it_be(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }