From 9b33e3d36fcd46072b9fe83f1121fb0fd87c0fd7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alexis=20Reigel=20=28=20=F0=9F=8C=B4=20may=202nd=20-=20may?=
 =?UTF-8?q?=209th=20=F0=9F=8C=B4=20=29?= <mail@koffeinfrei.org>
Date: Wed, 2 May 2018 08:08:16 +0000
Subject: [PATCH] Display and revoke active sessions

---
 Gemfile                                       |   3 +
 Gemfile.lock                                  |   2 +
 app/assets/stylesheets/framework/common.scss  |   1 +
 app/assets/stylesheets/framework/images.scss  |  35 +--
 .../profiles/active_sessions_controller.rb    |  14 ++
 app/helpers/active_sessions_helper.rb         |  23 ++
 app/models/active_session.rb                  | 110 +++++++++
 .../layouts/nav/sidebar/_profile.html.haml    |  11 +
 .../active_sessions/_active_session.html.haml |  31 +++
 .../profiles/active_sessions/index.html.haml  |  14 ++
 .../feature-display-active-sessions.yml       |   5 +
 config/initializers/session_store.rb          |  26 +--
 config/initializers/warden.rb                 |  12 +
 config/routes/profile.rb                      |   1 +
 doc/development/fe_guide/icons.md             |   2 +-
 doc/user/profile/active_sessions.md           |  20 ++
 doc/user/profile/img/active_sessions_list.png | Bin 0 -> 41649 bytes
 doc/user/profile/index.md                     |   1 +
 lib/gitlab/redis/shared_state.rb              |   2 +
 .../projects/clusters/gcp_controller_spec.rb  |   2 +-
 .../features/profiles/active_sessions_spec.rb |  89 ++++++++
 spec/features/users/active_sessions_spec.rb   |  69 ++++++
 spec/models/active_session_spec.rb            | 216 ++++++++++++++++++
 23 files changed, 642 insertions(+), 47 deletions(-)
 create mode 100644 app/controllers/profiles/active_sessions_controller.rb
 create mode 100644 app/helpers/active_sessions_helper.rb
 create mode 100644 app/models/active_session.rb
 create mode 100644 app/views/profiles/active_sessions/_active_session.html.haml
 create mode 100644 app/views/profiles/active_sessions/index.html.haml
 create mode 100644 changelogs/unreleased/feature-display-active-sessions.yml
 create mode 100644 doc/user/profile/active_sessions.md
 create mode 100644 doc/user/profile/img/active_sessions_list.png
 create mode 100644 spec/features/profiles/active_sessions_spec.rb
 create mode 100644 spec/features/users/active_sessions_spec.rb
 create mode 100644 spec/models/active_session_spec.rb

diff --git a/Gemfile b/Gemfile
index caeaae9616453..a68b044b39e86 100644
--- a/Gemfile
+++ b/Gemfile
@@ -184,6 +184,9 @@ gem 're2', '~> 1.1.1'
 
 gem 'version_sorter', '~> 2.1.0'
 
+# User agent parsing
+gem 'device_detector'
+
 # Cache
 gem 'redis-rails', '~> 5.0.2'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 9b2c47587ee13..f11df6a283e05 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -161,6 +161,7 @@ GEM
       activerecord (>= 3.2.0, < 5.1)
     descendants_tracker (0.0.4)
       thread_safe (~> 0.3, >= 0.3.1)
+    device_detector (1.0.0)
     devise (4.2.0)
       bcrypt (~> 3.0)
       orm_adapter (~> 0.1)
@@ -1026,6 +1027,7 @@ DEPENDENCIES
   database_cleaner (~> 1.5.0)
   deckar01-task_list (= 2.0.0)
   default_value_for (~> 3.0.0)
+  device_detector
   devise (~> 4.2)
   devise-two-factor (~> 3.0.0)
   diffy (~> 3.1.0)
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index e058a0b35b723..2faea55a5f5ac 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -452,6 +452,7 @@ img.emoji {
 
 /** COMMON CLASSES **/
 .prepend-top-0 { margin-top: 0; }
+.prepend-top-2 { margin-top: 2px; }
 .prepend-top-5 { margin-top: 5px; }
 .prepend-top-8 { margin-top: $grid-size; }
 .prepend-top-10 { margin-top: 10px; }
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 62a0fba3da393..ab3cceceae924 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -39,35 +39,10 @@
 svg {
   fill: currentColor;
 
-  &.s8 {
-    @include svg-size(8px);
-  }
-
-  &.s12 {
-    @include svg-size(12px);
-  }
-
-  &.s16 {
-    @include svg-size(16px);
-  }
-
-  &.s18 {
-    @include svg-size(18px);
-  }
-
-  &.s24 {
-    @include svg-size(24px);
-  }
-
-  &.s32 {
-    @include svg-size(32px);
-  }
-
-  &.s48 {
-    @include svg-size(48px);
-  }
-
-  &.s72 {
-    @include svg-size(72px);
+  $svg-sizes: 8 12 16 18 24 32 48 72;
+  @each $svg-size in $svg-sizes {
+    &.s#{$svg-size} {
+      @include svg-size(#{$svg-size}px);
+    }
   }
 }
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
new file mode 100644
index 0000000000000..f0cdc22836613
--- /dev/null
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -0,0 +1,14 @@
+class Profiles::ActiveSessionsController < Profiles::ApplicationController
+  def index
+    @sessions = ActiveSession.list(current_user)
+  end
+
+  def destroy
+    ActiveSession.destroy(current_user, params[:id])
+
+    respond_to do |format|
+      format.html { redirect_to profile_active_sessions_url, status: 302 }
+      format.js { head :ok }
+    end
+  end
+end
diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb
new file mode 100644
index 0000000000000..97b6dac67c504
--- /dev/null
+++ b/app/helpers/active_sessions_helper.rb
@@ -0,0 +1,23 @@
+module ActiveSessionsHelper
+  # Maps a device type as defined in `ActiveSession` to an svg icon name and
+  # outputs the icon html.
+  #
+  # see `DeviceDetector::Device::DEVICE_NAMES` about the available device types
+  def active_session_device_type_icon(active_session)
+    icon_name =
+      case active_session.device_type
+      when 'smartphone', 'feature phone', 'phablet'
+        'mobile'
+      when 'tablet'
+        'tablet'
+      when 'tv', 'smart display', 'camera', 'portable media player', 'console'
+        'media'
+      when 'car browser'
+        'car'
+      else
+        'monitor-o'
+      end
+
+    sprite_icon(icon_name, size: 16, css_class: 'prepend-top-2')
+  end
+end
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
new file mode 100644
index 0000000000000..b4a86dbb3315d
--- /dev/null
+++ b/app/models/active_session.rb
@@ -0,0 +1,110 @@
+class ActiveSession
+  include ActiveModel::Model
+
+  attr_accessor :created_at, :updated_at,
+    :session_id, :ip_address,
+    :browser, :os, :device_name, :device_type
+
+  def current?(session)
+    return false if session_id.nil? || session.id.nil?
+
+    session_id == session.id
+  end
+
+  def human_device_type
+    device_type&.titleize
+  end
+
+  def self.set(user, request)
+    Gitlab::Redis::SharedState.with do |redis|
+      session_id = request.session.id
+      client = DeviceDetector.new(request.user_agent)
+      timestamp = Time.current
+
+      active_user_session = new(
+        ip_address: request.ip,
+        browser: client.name,
+        os: client.os_name,
+        device_name: client.device_name,
+        device_type: client.device_type,
+        created_at: user.current_sign_in_at || timestamp,
+        updated_at: timestamp,
+        session_id: session_id
+      )
+
+      redis.pipelined do
+        redis.setex(
+          key_name(user.id, session_id),
+          Settings.gitlab['session_expire_delay'] * 60,
+          Marshal.dump(active_user_session)
+        )
+
+        redis.sadd(
+          lookup_key_name(user.id),
+          session_id
+        )
+      end
+    end
+  end
+
+  def self.list(user)
+    Gitlab::Redis::SharedState.with do |redis|
+      cleaned_up_lookup_entries(redis, user.id).map do |entry|
+        # rubocop:disable Security/MarshalLoad
+        Marshal.load(entry)
+        # rubocop:enable Security/MarshalLoad
+      end
+    end
+  end
+
+  def self.destroy(user, session_id)
+    Gitlab::Redis::SharedState.with do |redis|
+      redis.srem(lookup_key_name(user.id), session_id)
+
+      deleted_keys = redis.del(key_name(user.id, session_id))
+
+      # only allow deleting the devise session if we could actually find a
+      # related active session. this prevents another user from deleting
+      # someone else's session.
+      if deleted_keys > 0
+        redis.del("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}")
+      end
+    end
+  end
+
+  def self.cleanup(user)
+    Gitlab::Redis::SharedState.with do |redis|
+      cleaned_up_lookup_entries(redis, user.id)
+    end
+  end
+
+  def self.key_name(user_id, session_id = '*')
+    "#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
+  end
+
+  def self.lookup_key_name(user_id)
+    "#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
+  end
+
+  def self.cleaned_up_lookup_entries(redis, user_id)
+    lookup_key = lookup_key_name(user_id)
+
+    session_ids = redis.smembers(lookup_key)
+
+    entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
+    return [] if entry_keys.empty?
+
+    entries = redis.mget(entry_keys)
+
+    session_ids_and_entries = session_ids.zip(entries)
+
+    # remove expired keys.
+    # only the single key entries are automatically expired by redis, the
+    # lookup entries in the set need to be removed manually.
+    session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry|
+      redis.srem(lookup_key, session_id)
+    end
+
+    session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry }
+  end
+end
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index c878fcf28089b..6cbd163dd41b9 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -129,6 +129,17 @@
             = link_to profile_preferences_path do
               %strong.fly-out-top-item-name
                 #{ _('Preferences') }
+      = nav_link(controller: :active_sessions) do
+        = link_to profile_active_sessions_path do
+          .nav-icon-container
+            = sprite_icon('monitor-lines')
+          %span.nav-item-name
+            Active Sessions
+        %ul.sidebar-sub-level-items.is-fly-out-only
+          = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" } ) do
+            = link_to profile_active_sessions_path do
+              %strong.fly-out-top-item-name
+                #{ _('Active Sessions') }
       = nav_link(path: 'profiles#audit_log') do
         = link_to audit_log_profile_path do
           .nav-icon-container
diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml
new file mode 100644
index 0000000000000..d40b771f48bd9
--- /dev/null
+++ b/app/views/profiles/active_sessions/_active_session.html.haml
@@ -0,0 +1,31 @@
+- is_current_session = active_session.current?(session)
+
+%li
+  .pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
+    = active_session_device_type_icon(active_session)
+
+  .description.pull-left
+    %div
+      %strong= active_session.ip_address
+    - if is_current_session
+      %div This is your current session
+    - else
+      %div
+        Last accessed on
+        = l(active_session.updated_at, format: :short)
+
+    %div
+      %strong= active_session.browser
+      on
+      %strong= active_session.os
+
+    %div
+      %strong Signed in
+      on
+      = l(active_session.created_at, format: :short)
+
+  - unless is_current_session
+    .pull-right
+      = link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
+        %span.sr-only Revoke
+        Revoke
diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml
new file mode 100644
index 0000000000000..d0250bb4eab31
--- /dev/null
+++ b/app/views/profiles/active_sessions/index.html.haml
@@ -0,0 +1,14 @@
+- page_title 'Active Sessions'
+- @content_class = "limit-container-width" unless fluid_layout
+
+.row.prepend-top-default
+  .col-lg-4.profile-settings-sidebar
+    %h4.prepend-top-0
+      = page_title
+    %p
+      This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.
+  .col-lg-8
+    .append-bottom-default
+
+      %ul.well-list
+        = render partial: 'profiles/active_sessions/active_session', collection: @sessions
diff --git a/changelogs/unreleased/feature-display-active-sessions.yml b/changelogs/unreleased/feature-display-active-sessions.yml
new file mode 100644
index 0000000000000..14cfa66953eb4
--- /dev/null
+++ b/changelogs/unreleased/feature-display-active-sessions.yml
@@ -0,0 +1,5 @@
+---
+title: Display active sessions and allow the user to revoke any of it
+merge_request: 17867
+author: Alexis Reigel
+type: added
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index f2fde1e0048f0..da24881885ec2 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -15,19 +15,15 @@
                "_gitlab_session"
              end
 
-if Rails.env.test?
-  Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
-else
-  sessions_config = Gitlab::Redis::SharedState.params
-  sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE
+sessions_config = Gitlab::Redis::SharedState.params
+sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE
 
-  Gitlab::Application.config.session_store(
-    :redis_store, # Using the cookie_store would enable session replay attacks.
-    servers: sessions_config,
-    key: cookie_key,
-    secure: Gitlab.config.gitlab.https,
-    httponly: true,
-    expires_in: Settings.gitlab['session_expire_delay'] * 60,
-    path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root
-  )
-end
+Gitlab::Application.config.session_store(
+  :redis_store, # Using the cookie_store would enable session replay attacks.
+  servers: sessions_config,
+  key: cookie_key,
+  secure: Gitlab.config.gitlab.https,
+  httponly: true,
+  expires_in: Settings.gitlab['session_expire_delay'] * 60,
+  path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root
+)
diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb
index ee034d21eaedd..bf079f8e1a7e2 100644
--- a/config/initializers/warden.rb
+++ b/config/initializers/warden.rb
@@ -6,4 +6,16 @@
   Warden::Manager.before_failure do |env, opts|
     Gitlab::Auth::BlockedUserTracker.log_if_user_blocked(env)
   end
+
+  Warden::Manager.after_authentication do |user, auth, opts|
+    ActiveSession.cleanup(user)
+  end
+
+  Warden::Manager.after_set_user only: :fetch do |user, auth, opts|
+    ActiveSession.set(user, auth.request)
+  end
+
+  Warden::Manager.before_logout do |user, auth, opts|
+    ActiveSession.destroy(user || auth.user, auth.request.session.id)
+  end
 end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index bcfc17a5f667c..a9ba5ac2c0b69 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -30,6 +30,7 @@
         put :revoke
       end
     end
+    resources :active_sessions, only: [:index, :destroy]
     resources :emails, only: [:index, :create, :destroy] do
       member do
         put :resend_confirmation_instructions
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
index b288ee9572254..b469a9c6aef4a 100644
--- a/doc/development/fe_guide/icons.md
+++ b/doc/development/fe_guide/icons.md
@@ -49,7 +49,7 @@ Please use the following function inside JS to render an icon :
 
 All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
 
-To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced.
+To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs`.
 
 # SVG Illustrations
 
diff --git a/doc/user/profile/active_sessions.md b/doc/user/profile/active_sessions.md
new file mode 100644
index 0000000000000..5119c0e30d056
--- /dev/null
+++ b/doc/user/profile/active_sessions.md
@@ -0,0 +1,20 @@
+# Active Sessions
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17867)
+>   in GitLab 10.8.
+
+GitLab lists all devices that have logged into your account. This allows you to
+review the sessions and revoke any of it that you don't recognize.
+
+## Listing all active sessions
+
+1. On the upper right corner, click on your avatar and go to your **Settings**.
+1. Navigate to the **Active Sessions** tab.
+
+![Active sessions list](img/active_sessions_list.png)
+
+## Revoking a session
+
+1. Navigate to your [profile's](#profile-settings) **Settings > Active Sessions**.
+1. Click on **Revoke** besides a session. The current session cannot be
+   revoked, as this would sign you out of GitLab.
diff --git a/doc/user/profile/img/active_sessions_list.png b/doc/user/profile/img/active_sessions_list.png
new file mode 100644
index 0000000000000000000000000000000000000000..76a52220bcd12ddbee6339961497015b1647b633
GIT binary patch
literal 41649
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

literal 0
HcmV?d00001

diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index ab16f8d14c125..91cdef8d1dd79 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -39,6 +39,7 @@ From there, you can:
 - Manage [SSH keys](../../ssh/README.md#ssh) to access your account via SSH
 - Manage your [preferences](preferences.md#syntax-highlighting-theme)
 to customize your own GitLab experience
+- [View your active sessions](active_sessions.md) and revoke any of them if necessary
 - Access your audit log, a security log of important events involving your account
 
 ## Changing your username
diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb
index 10bec7a90dafe..e5a0fdae7ef4d 100644
--- a/lib/gitlab/redis/shared_state.rb
+++ b/lib/gitlab/redis/shared_state.rb
@@ -5,6 +5,8 @@ module Gitlab
   module Redis
     class SharedState < ::Gitlab::Redis::Wrapper
       SESSION_NAMESPACE = 'session:gitlab'.freeze
+      USER_SESSIONS_NAMESPACE = 'session:user:gitlab'.freeze
+      USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'.freeze
       DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze
       REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze
 
diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb
index e14ba29fa70ab..715bb9f5e5218 100644
--- a/spec/controllers/projects/clusters/gcp_controller_spec.rb
+++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb
@@ -142,7 +142,7 @@ def go
 
         context 'when google project billing is enabled' do
           before do
-            redis_double = double
+            redis_double = double.as_null_object
             allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double)
             allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true')
           end
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb
new file mode 100644
index 0000000000000..4045cfd21c452
--- /dev/null
+++ b/spec/features/profiles/active_sessions_spec.rb
@@ -0,0 +1,89 @@
+require 'rails_helper'
+
+feature 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
+  let(:user) do
+    create(:user).tap do |user|
+      user.current_sign_in_at = Time.current
+    end
+  end
+
+  around do |example|
+    Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
+      example.run
+    end
+  end
+
+  scenario 'User sees their active sessions' do
+    Capybara::Session.new(:session1)
+    Capybara::Session.new(:session2)
+
+    # note: headers can only be set on the non-js (aka. rack-test) driver
+    using_session :session1 do
+      Capybara.page.driver.header(
+        'User-Agent',
+        'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0'
+      )
+
+      gitlab_sign_in(user)
+    end
+
+    # set an additional session on another device
+    using_session :session2 do
+      Capybara.page.driver.header(
+        'User-Agent',
+        'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]'
+      )
+
+      gitlab_sign_in(user)
+    end
+
+    using_session :session1 do
+      visit profile_active_sessions_path
+
+      expect(page).to have_content(
+        '127.0.0.1 ' \
+        'This is your current session ' \
+        'Firefox on Ubuntu ' \
+        'Signed in on 12 Mar 09:06'
+      )
+
+      expect(page).to have_selector '[title="Desktop"]', count: 1
+
+      expect(page).to have_content(
+        '127.0.0.1 ' \
+        'Last accessed on 12 Mar 09:06 ' \
+        'Mobile Safari on iOS ' \
+        'Signed in on 12 Mar 09:06'
+      )
+
+      expect(page).to have_selector '[title="Smartphone"]', count: 1
+    end
+  end
+
+  scenario 'User can revoke a session', :js, :redis_session_store do
+    Capybara::Session.new(:session1)
+    Capybara::Session.new(:session2)
+
+    # set an additional session in another browser
+    using_session :session2 do
+      gitlab_sign_in(user)
+    end
+
+    using_session :session1 do
+      gitlab_sign_in(user)
+      visit profile_active_sessions_path
+
+      expect(page).to have_link('Revoke', count: 1)
+
+      accept_confirm { click_on 'Revoke' }
+
+      expect(page).not_to have_link('Revoke')
+    end
+
+    using_session :session2 do
+      visit profile_active_sessions_path
+
+      expect(page).to have_content('You need to sign in or sign up before continuing.')
+    end
+  end
+end
diff --git a/spec/features/users/active_sessions_spec.rb b/spec/features/users/active_sessions_spec.rb
new file mode 100644
index 0000000000000..631d7e3bcedd8
--- /dev/null
+++ b/spec/features/users/active_sessions_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+feature 'Active user sessions', :clean_gitlab_redis_shared_state do
+  scenario 'Successful login adds a new active user login' do
+    now = Time.zone.parse('2018-03-12 09:06')
+    Timecop.freeze(now) do
+      user = create(:user)
+      gitlab_sign_in(user)
+      expect(current_path).to eq root_path
+
+      sessions = ActiveSession.list(user)
+      expect(sessions.count).to eq 1
+
+      # refresh the current page updates the updated_at
+      Timecop.freeze(now + 1.minute) do
+        visit current_path
+
+        sessions = ActiveSession.list(user)
+        expect(sessions.first).to have_attributes(
+          created_at: Time.zone.parse('2018-03-12 09:06'),
+          updated_at: Time.zone.parse('2018-03-12 09:07')
+        )
+      end
+    end
+  end
+
+  scenario 'Successful login cleans up obsolete entries' do
+    user = create(:user)
+
+    Gitlab::Redis::SharedState.with do |redis|
+      redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+    end
+
+    gitlab_sign_in(user)
+
+    Gitlab::Redis::SharedState.with do |redis|
+      expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).not_to include '59822c7d9fcdfa03725eff41782ad97d'
+    end
+  end
+
+  scenario 'Sessionless login does not clean up obsolete entries' do
+    user = create(:user)
+    personal_access_token = create(:personal_access_token, user: user)
+
+    Gitlab::Redis::SharedState.with do |redis|
+      redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+    end
+
+    visit user_path(user, :atom, private_token: personal_access_token.token)
+    expect(page.status_code).to eq 200
+
+    Gitlab::Redis::SharedState.with do |redis|
+      expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to include '59822c7d9fcdfa03725eff41782ad97d'
+    end
+  end
+
+  scenario 'Logout deletes the active user login' do
+    user = create(:user)
+    gitlab_sign_in(user)
+    expect(current_path).to eq root_path
+
+    expect(ActiveSession.list(user).count).to eq 1
+
+    gitlab_sign_out
+    expect(current_path).to eq new_user_session_path
+
+    expect(ActiveSession.list(user)).to be_empty
+  end
+end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
new file mode 100644
index 0000000000000..129b2f926831b
--- /dev/null
+++ b/spec/models/active_session_spec.rb
@@ -0,0 +1,216 @@
+require 'rails_helper'
+
+RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
+  let(:user) do
+    create(:user).tap do |user|
+      user.current_sign_in_at = Time.current
+    end
+  end
+
+  let(:session) { double(:session, id: '6919a6f1bb119dd7396fadc38fd18d0d') }
+
+  let(:request) do
+    double(:request, {
+      user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 ' \
+        '(KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]',
+      ip: '127.0.0.1',
+      session: session
+    })
+  end
+
+  describe '#current?' do
+    it 'returns true if the active session matches the current session' do
+      active_session = ActiveSession.new(session_id: '6919a6f1bb119dd7396fadc38fd18d0d')
+
+      expect(active_session.current?(session)).to be true
+    end
+
+    it 'returns false if the active session does not match the current session' do
+      active_session = ActiveSession.new(session_id: '59822c7d9fcdfa03725eff41782ad97d')
+
+      expect(active_session.current?(session)).to be false
+    end
+
+    it 'returns false if the session id is nil' do
+      active_session = ActiveSession.new(session_id: nil)
+      session = double(:session, id: nil)
+
+      expect(active_session.current?(session)).to be false
+    end
+  end
+
+  describe '.list' do
+    it 'returns all sessions by user' do
+      Gitlab::Redis::SharedState.with do |redis|
+        redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
+        redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", Marshal.dump({ session_id: 'b' }))
+        redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
+
+        redis.sadd(
+          "session:lookup:user:gitlab:#{user.id}",
+          %w[
+            6919a6f1bb119dd7396fadc38fd18d0d
+            59822c7d9fcdfa03725eff41782ad97d
+          ]
+        )
+      end
+
+      expect(ActiveSession.list(user)).to match_array [{ session_id: 'a' }, { session_id: 'b' }]
+    end
+
+    it 'does not return obsolete entries and cleans them up' do
+      Gitlab::Redis::SharedState.with do |redis|
+        redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
+
+        redis.sadd(
+          "session:lookup:user:gitlab:#{user.id}",
+          %w[
+            6919a6f1bb119dd7396fadc38fd18d0d
+            59822c7d9fcdfa03725eff41782ad97d
+          ]
+        )
+      end
+
+      expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }]
+
+      Gitlab::Redis::SharedState.with do |redis|
+        expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
+      end
+    end
+
+    it 'returns an empty array if the use does not have any active session' do
+      expect(ActiveSession.list(user)).to eq []
+    end
+  end
+
+  describe '.set' do
+    it 'sets a new redis entry for the user session and a lookup entry' do
+      ActiveSession.set(user, request)
+
+      Gitlab::Redis::SharedState.with do |redis|
+        expect(redis.scan_each.to_a).to match_array [
+          "session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d",
+          "session:lookup:user:gitlab:#{user.id}"
+        ]
+      end
+    end
+
+    it 'adds timestamps and information from the request' do
+      Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
+        ActiveSession.set(user, request)
+
+        session = ActiveSession.list(user)
+
+        expect(session.count).to eq 1
+        expect(session.first).to have_attributes(
+          ip_address: '127.0.0.1',
+          browser: 'Mobile Safari',
+          os: 'iOS',
+          device_name: 'iPhone 6',
+          device_type: 'smartphone',
+          created_at: Time.zone.parse('2018-03-12 09:06'),
+          updated_at: Time.zone.parse('2018-03-12 09:06'),
+          session_id: '6919a6f1bb119dd7396fadc38fd18d0d'
+        )
+      end
+    end
+
+    it 'keeps the created_at from the login on consecutive requests' do
+      now = Time.zone.parse('2018-03-12 09:06')
+
+      Timecop.freeze(now) do
+        ActiveSession.set(user, request)
+
+        Timecop.freeze(now + 1.minute) do
+          ActiveSession.set(user, request)
+
+          session = ActiveSession.list(user)
+
+          expect(session.first).to have_attributes(
+            created_at: Time.zone.parse('2018-03-12 09:06'),
+            updated_at: Time.zone.parse('2018-03-12 09:07')
+          )
+        end
+      end
+    end
+  end
+
+  describe '.destroy' do
+    it 'removes the entry associated with the currently killed user session' do
+      Gitlab::Redis::SharedState.with do |redis|
+        redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+        redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", '')
+        redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
+      end
+
+      ActiveSession.destroy(user, request.session.id)
+
+      Gitlab::Redis::SharedState.with do |redis|
+        expect(redis.scan_each(match: "session:user:gitlab:*")).to match_array [
+          "session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d",
+          "session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358"
+        ]
+      end
+    end
+
+    it 'removes the lookup entry' do
+      Gitlab::Redis::SharedState.with do |redis|
+        redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+        redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
+      end
+
+      ActiveSession.destroy(user, request.session.id)
+
+      Gitlab::Redis::SharedState.with do |redis|
+        expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
+      end
+    end
+
+    it 'removes the devise session' do
+      Gitlab::Redis::SharedState.with do |redis|
+        redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+        redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '')
+      end
+
+      ActiveSession.destroy(user, request.session.id)
+
+      Gitlab::Redis::SharedState.with do |redis|
+        expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
+      end
+    end
+
+    it 'does not remove the devise session if the active session could not be found' do
+      Gitlab::Redis::SharedState.with do |redis|
+        redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '')
+      end
+
+      other_user = create(:user)
+
+      ActiveSession.destroy(other_user, request.session.id)
+
+      Gitlab::Redis::SharedState.with do |redis|
+        expect(redis.scan_each(match: "session:gitlab:*").to_a).not_to be_empty
+      end
+    end
+  end
+
+  describe '.cleanup' do
+    it 'removes obsolete lookup entries' do
+      Gitlab::Redis::SharedState.with do |redis|
+        redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+        redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
+        redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+      end
+
+      ActiveSession.cleanup(user)
+
+      Gitlab::Redis::SharedState.with do |redis|
+        expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
+      end
+    end
+
+    it 'does not bail if there are no lookup entries' do
+      ActiveSession.cleanup(user)
+    end
+  end
+end
-- 
GitLab