diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index a28a87ed4f8b16e0f97cf77c0f783800e9c4f367..905ecbff57c832a2a580caec4489d277fb619778 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -24,3 +24,14 @@
 .service-settings .control-label {
   padding-top: 0;
 }
+
+.personal-access-token-token-container {
+  #personal-access-token-token {
+    width: 80%;
+    display: inline;
+  }
+
+  .btn-clipboard {
+    margin-left: 5px;
+  }
+}
diff --git a/app/controllers/admin/personal_access_tokens_controller.rb b/app/controllers/admin/personal_access_tokens_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7202d80ce1b70811faab6451b2fe8ce24d0b766a
--- /dev/null
+++ b/app/controllers/admin/personal_access_tokens_controller.rb
@@ -0,0 +1,48 @@
+class Admin::PersonalAccessTokensController < Admin::ApplicationController
+  before_action :user
+
+  def index
+    set_index_vars
+  end
+
+  def create
+    @personal_access_token = user.personal_access_tokens.generate(personal_access_token_params)
+
+    if @personal_access_token.save
+      flash[:personal_access_token] = @personal_access_token.token
+      redirect_to admin_user_personal_access_tokens_path, notice: "A new personal access token has been created."
+    else
+      set_index_vars
+      render :index
+    end
+  end
+
+  def revoke
+    @personal_access_token = user.personal_access_tokens.find(params[:id])
+
+    if @personal_access_token.revoke!
+      flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
+    else
+      flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}."
+    end
+
+    redirect_to admin_user_personal_access_tokens_path
+  end
+
+  private
+
+  def user
+    @user ||= User.find_by!(username: params[:user_id])
+  end
+
+  def personal_access_token_params
+    params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: [])
+  end
+
+  def set_index_vars
+    @personal_access_token ||= user.personal_access_tokens.build
+    @scopes = Gitlab::Auth::SCOPES
+    @active_personal_access_tokens = PersonalAccessToken.and_impersonation_tokens.where(user_id: user.id).active.order(:expires_at)
+    @inactive_personal_access_tokens = PersonalAccessToken.and_impersonation_tokens.where(user_id: user.id).inactive
+  end
+end
diff --git a/app/views/admin/personal_access_tokens/_form.html.haml b/app/views/admin/personal_access_tokens/_form.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d194a0fd5119f0ff5b83f7677954d7072d1e5384
--- /dev/null
+++ b/app/views/admin/personal_access_tokens/_form.html.haml
@@ -0,0 +1,28 @@
+- personal_access_token = local_assigns.fetch(:personal_access_token)
+- scopes = local_assigns.fetch(:scopes)
+
+= form_for [:admin_user, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f|
+
+  = form_errors(personal_access_token)
+
+  .form-group
+    = f.label :name, class: 'label-light'
+    = f.text_field :name, class: "form-control", required: true
+
+  .form-group
+    = f.label :expires_at, class: 'label-light'
+    = f.text_field :expires_at, class: "datepicker form-control"
+
+  .form-group
+    = f.label :scopes, class: 'label-light'
+    = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes
+
+  .form-group
+    = f.label :impersonation, class: 'label-light'
+    %fieldset
+      = f.check_box :impersonation
+      = f.label 'impersonation', 'You can impersonate the user'
+      %span= "(Normal users will not see this type of token)"
+
+  .prepend-top-default
+    = f.submit 'Create Personal Access Token', class: "btn btn-create"
diff --git a/app/views/admin/personal_access_tokens/index.html.haml b/app/views/admin/personal_access_tokens/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..90aade17e1ba90091eaf1121cc0cd9fc319a40a5
--- /dev/null
+++ b/app/views/admin/personal_access_tokens/index.html.haml
@@ -0,0 +1,80 @@
+- page_title "Personal Access Tokens"
+= render 'admin/users/head'
+
+.row.prepend-top-default
+  .col-lg-12
+
+    %h5.prepend-top-0
+      Add a Personal Access Token
+    %p.profile-settings-content
+      Pick a name for the application, and we'll give you a unique token.
+
+    = render "form", personal_access_token: @personal_access_token, scopes: @scopes
+
+    %hr
+
+    %h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length})
+
+    - if @active_personal_access_tokens.present?
+      .table-responsive
+        %table.table.active-personal-access-tokens
+          %thead
+            %tr
+              %th Name
+              %th Created
+              %th Expires
+              %th Scopes
+              %th Token
+              %th Impersonation
+              %th
+          %tbody
+            - @active_personal_access_tokens.each do |personal_access_token|
+              %tr
+                %td= personal_access_token.name
+                %td= personal_access_token.created_at.to_date.to_s(:medium)
+                %td
+                  - if personal_access_token.expires?
+                    %span{ class: ('text-warning' if personal_access_token.expires_soon?) }
+                      In #{distance_of_time_in_words_to_now(personal_access_token.expires_at)}
+                  - else
+                    %span.personal-access-personal_access_tokens-never-expires-label Never
+                %td= personal_access_token.scopes.present? ? personal_access_token.scopes.join(", ") : "<no scopes selected>"
+                %td.personal-access-token-token-container
+                  = text_field_tag 'personal-access-token-token', personal_access_token.token, readonly: true, class: "form-control"
+                  = clipboard_button(clipboard_text: personal_access_token.token)
+                %td= personal_access_token.impersonation
+                %td= link_to "Revoke", revoke_admin_user_personal_access_token_path(id: personal_access_token.id, user_id: personal_access_token.user.username), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
+
+    - else
+      .settings-message.text-center
+        This user has no active tokens.
+
+    %hr
+
+    %h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length})
+
+    - if @inactive_personal_access_tokens.present?
+      .table-responsive
+        %table.table.inactive-personal-access-tokens
+          %thead
+            %tr
+              %th Name
+              %th Created
+          %tbody
+            - @inactive_personal_access_tokens.each do |token|
+              %tr
+                %td= token.name
+                %td= token.created_at.to_date.to_s(:medium)
+
+    - else
+      .settings-message.text-center
+        This user has no inactive tokens.
+
+
+:javascript
+  var date = $('#personal_access_token_expires_at').val();
+
+  var datepicker = $(".datepicker").datepicker({
+    dateFormat: "yy-mm-dd",
+    minDate: 0
+  });
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 9984e733956afeaf1a0a62983db62be2fd22422f..c95ae93b710af8304adc80025d90f4d8f5431814 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -21,4 +21,6 @@
     = link_to "SSH keys", keys_admin_user_path(@user)
   = nav_link(controller: :identities) do
     = link_to "Identities", admin_user_identities_path(@user)
+  = nav_link(controller: :personal_access_tokens) do
+    = link_to "Access Tokens", admin_user_personal_access_tokens_path(@user)
 .append-bottom-default
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 8e99239f350d6ec40cf6dea1837690c6bd9c6bfe..6d2748df386c2af1ca1d3661d7cab454bd3028ed 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -2,6 +2,11 @@ namespace :admin do
   resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
     resources :keys, only: [:show, :destroy]
     resources :identities, except: [:show]
+    resources :personal_access_tokens, only: [:index, :create] do
+      member do
+        put :revoke
+      end
+    end
 
     member do
       get :projects
diff --git a/spec/features/admin/admin_users_personal_access_tokens_spec.rb b/spec/features/admin/admin_users_personal_access_tokens_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b7ec8c9fe863de95b9f9e6d0d49a6786b370991b
--- /dev/null
+++ b/spec/features/admin/admin_users_personal_access_tokens_spec.rb
@@ -0,0 +1,95 @@
+require 'spec_helper'
+
+describe 'Admin > Users > Personal Access Tokens', feature: true, js: true do
+  let(:admin) { create(:admin) }
+  let!(:user) { create(:user) }
+
+  def active_personal_access_tokens
+    find(".table.active-personal-access-tokens")
+  end
+
+  def inactive_personal_access_tokens
+    find(".table.inactive-personal-access-tokens")
+  end
+
+  def created_personal_access_token
+    find("#created-personal-access-token").value
+  end
+
+  def disallow_personal_access_token_saves!
+    allow_any_instance_of(PersonalAccessToken).to receive(:save).and_return(false)
+    errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") }
+    allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors)
+  end
+
+  before do
+    login_as(admin)
+  end
+
+  describe "token creation" do
+    it "allows creation of a token" do
+      name = FFaker::Product.brand
+
+      visit admin_user_personal_access_tokens_path(user_id: user.username)
+      fill_in "Name", with: name
+
+      # Set date to 1st of next month
+      find_field("Expires at").trigger('focus')
+      find("a[title='Next']").click
+      click_on "1"
+
+      # Scopes
+      check "api"
+      check "read_user"
+
+      check "You can impersonate the user"
+
+      click_on "Create Personal Access Token"
+      expect(active_personal_access_tokens).to have_text(name)
+      expect(active_personal_access_tokens).to have_text('In')
+      expect(active_personal_access_tokens).to have_text('api')
+      expect(active_personal_access_tokens).to have_text('read_user')
+      expect(active_personal_access_tokens).to have_text('true')
+    end
+
+    context "when creation fails" do
+      it "displays an error message" do
+        disallow_personal_access_token_saves!
+        visit admin_user_personal_access_tokens_path(user_id: user.username)
+        fill_in "Name", with: FFaker::Product.brand
+
+        expect { click_on "Create Personal Access Token" }.not_to change { PersonalAccessToken.count }
+        expect(page).to have_content("Name cannot be nil")
+      end
+    end
+  end
+
+  describe "inactive tokens" do
+    let!(:personal_access_token) { create(:personal_access_token, user: user) }
+
+    it "allows revocation of an active token" do
+      visit admin_user_personal_access_tokens_path(user_id: user.username)
+      click_on "Revoke"
+
+      expect(inactive_personal_access_tokens).to have_text(personal_access_token.name)
+    end
+
+    it "moves expired tokens to the 'inactive' section" do
+      personal_access_token.update(expires_at: 5.days.ago)
+      visit admin_user_personal_access_tokens_path(user_id: user.username)
+
+      expect(inactive_personal_access_tokens).to have_text(personal_access_token.name)
+    end
+
+    context "when revocation fails" do
+      it "displays an error message" do
+        disallow_personal_access_token_saves!
+        visit admin_user_personal_access_tokens_path(user_id: user.username)
+
+        click_on "Revoke"
+        expect(active_personal_access_tokens).to have_text(personal_access_token.name)
+        expect(page).to have_content("Could not revoke")
+      end
+    end
+  end
+end